seed_dummy_data.ts 339 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802
  1. import { Prisma, PrismaClient } from "../src/generated/prisma/client";
  2. import { PrismaPg } from "@prisma/adapter-pg";
  3. import { createCipheriv, createHash, hkdfSync, randomBytes, scryptSync } from "node:crypto";
  4. import { deflateSync } from "node:zlib";
  5. import * as fs from "fs";
  6. import * as path from "path";
  7. import * as https from "https";
  8. import * as http from "http";
  9. // Matches better-auth's scrypt params — output is byte-compatible with its hashPassword,
  10. // so the stored hash is verifiable by better-auth at sign-in time.
  11. function hashPassword(password: string): string {
  12. const N = 16384, r = 16, p = 1, dkLen = 64;
  13. const maxmem = 128 * N * r * 2;
  14. const salt = randomBytes(16).toString("hex");
  15. const key = scryptSync(password.normalize("NFKC"), salt, dkLen, { N, r, p, maxmem });
  16. return `${salt}:${key.toString("hex")}`;
  17. }
  18. // Copy of src/features/integrations/Lib/vault.ts sealCredentials(). The image
  19. // does not ship that file, and the app must be able to open what is sealed
  20. // here, so the format (v1.iv.tag.data, AES-256-GCM) and the key derivation
  21. // (INTEGRATIONS_ENCRYPTION_KEY, else HKDF of BETTER_AUTH_SECRET) match exactly.
  22. function sealCredentials(value: Record<string, unknown>): string {
  23. const explicit = process.env.INTEGRATIONS_ENCRYPTION_KEY?.trim();
  24. let key: Buffer;
  25. if (explicit) {
  26. if (!/^[0-9a-f]{64}$/i.test(explicit)) throw new Error("INTEGRATIONS_ENCRYPTION_KEY must be 64 hex characters");
  27. key = Buffer.from(explicit, "hex");
  28. } else {
  29. const authSecret = process.env.BETTER_AUTH_SECRET;
  30. if (!authSecret) throw new Error("Set BETTER_AUTH_SECRET or INTEGRATIONS_ENCRYPTION_KEY to seed integration connections");
  31. key = Buffer.from(hkdfSync("sha256", authSecret, "torqvoice", "integrations-vault", 32));
  32. }
  33. const iv = randomBytes(12);
  34. const cipher = createCipheriv("aes-256-gcm", key, iv);
  35. const encrypted = Buffer.concat([cipher.update(Buffer.from(JSON.stringify(value), "utf8")), cipher.final()]);
  36. return ["v1", iv.toString("base64url"), cipher.getAuthTag().toString("base64url"), encrypted.toString("base64url")].join(".");
  37. }
  38. const prisma = new PrismaClient({
  39. adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
  40. });
  41. // ── Clock ────────────────────────────────────────────────────────────────
  42. // Every date below is derived from the moment the seed runs, so a reset always
  43. // produces a workshop that is busy *today*: jobs on the board this week, quotes
  44. // about to expire, reminders coming due. Nothing is pinned to a calendar date,
  45. // which is what lets demo.torqvoice.com reset on a schedule and still look live.
  46. const NOW = new Date();
  47. const TODAY = (() => { const d = new Date(NOW); d.setHours(0, 0, 0, 0); return d; })();
  48. /** Midnight, `n` days from today. Negative is the past. */
  49. function days(n: number): Date {
  50. const d = new Date(TODAY);
  51. d.setDate(d.getDate() + n);
  52. return d;
  53. }
  54. /** `n` days from today at a wall-clock time. */
  55. function at(n: number, hour: number, minute = 0): Date {
  56. const d = days(n);
  57. d.setHours(hour, minute, 0, 0);
  58. return d;
  59. }
  60. /** Offsets from the current instant - for message and chat timelines. */
  61. function hoursAgo(n: number): Date { return new Date(NOW.getTime() - n * 3600_000); }
  62. function minutesAgo(n: number): Date { return new Date(NOW.getTime() - n * 60_000); }
  63. /** Same day-of-month, `n` months out. Used for recurring schedules. */
  64. function months(n: number): Date {
  65. const d = new Date(TODAY);
  66. d.setMonth(d.getMonth() + n);
  67. return d;
  68. }
  69. /**
  70. * Monday of the week the work board showcases. On a weekday that is this
  71. * week's Monday, so the board's day view lands on a busy day. Run over a
  72. * weekend it rolls to the coming Monday rather than showing a spent week.
  73. */
  74. function weekMonday(): Date {
  75. const dow = NOW.getDay(); // 0 = Sunday
  76. const toMonday = dow === 0 ? 1 : dow === 6 ? 2 : 1 - dow;
  77. return days(toMonday);
  78. }
  79. /** Day offset from today to the board's Monday - for spreading other data. */
  80. const MONDAY_OFFSET = Math.round((weekMonday().getTime() - TODAY.getTime()) / 86_400_000);
  81. const YEAR = TODAY.getFullYear();
  82. const USER_ID = "IqcAL6GKoedJNO8Xi6UJkCM5YZqgS9uU";
  83. const ORG_ID = "cmmh0vczm0000oiyan37vuyqf";
  84. const DEMO_EMAIL = process.env.DEMO_USER_EMAIL || "demo@torqvoice.com";
  85. const DEMO_PASSWORD = process.env.DEMO_USER_PASSWORD || "demo";
  86. const DEMO_ORG_NAME = process.env.DEMO_ORG_NAME || "Demo Auto Workshop";
  87. /// What this workshop calls worn out. Stored in millimetres whatever the
  88. /// unit system, and used both for the settings rows and for grading the
  89. /// seeded readings, so the two can never disagree.
  90. const DEMO_TREAD_LIMITS = { summerReplace: 1.6, winterReplace: 4, warnMargin: 1 };
  91. /// The four values a work order's `status` column stores (SYSTEM_STATUSES in
  92. /// src/features/work-order-statuses/Lib/stages.ts, and the enum in
  93. /// serviceSchema.ts). Anything else, "scheduled" or "in_progress" say, is read
  94. /// by the app as not started and matches no filter, so every job the seed
  95. /// writes is typed with this rather than a plain string.
  96. type JobStatus = "pending" | "in-progress" | "waiting-parts" | "completed";
  97. /// The workshop's warranty, written once and used for the Settings -> Warranty
  98. /// defaults, every seeded invoice and every seeded quote, so they all say the
  99. /// same thing.
  100. const WARRANTY = {
  101. months: 12,
  102. mileage: 12000,
  103. terms:
  104. "Parts and labor on this repair are guaranteed for 12 months or 12,000 miles, whichever comes first. Wear items, and damage from misuse, accidents or later modification, are not covered.",
  105. } as const;
  106. /// The warranty columns a finished job or a quote carries. `warrantyStatus`
  107. /// is what the printed sheets and the warranty panel read first.
  108. function warrantyColumns(from: Date | null) {
  109. const expires = from ? new Date(from) : null;
  110. expires?.setMonth(expires.getMonth() + WARRANTY.months);
  111. return {
  112. warrantyStatus: "included",
  113. warrantyMonths: WARRANTY.months,
  114. warrantyMileage: WARRANTY.mileage,
  115. warrantyNotes: WARRANTY.terms,
  116. ...(expires ? { warrantyExpiresAt: expires } : {}),
  117. };
  118. }
  119. /// The "certificate-regulator" layout from src/features/settings/Schema/
  120. /// layoutPresets.ts, as buildLayoutFromPreset() builds it, with version set to
  121. /// DESIGNER_LAYOUT_VERSION (3). Generated from that code, not written by hand:
  122. /// regenerate it there if the preset or the layout version changes.
  123. const CERTIFICATE_REGULATOR_LAYOUT = JSON.stringify({"sections":[{"id":"header","visible":true,"order":0,"fields":[{"id":"logo","visible":true},{"id":"company_name","visible":true},{"id":"company_address","visible":true},{"id":"company_phone","visible":true},{"id":"company_email","visible":true},{"id":"company_org_number","visible":true}]},{"id":"document_title","visible":true,"order":1,"fields":[{"id":"title","visible":true},{"id":"invoice_number","visible":true},{"id":"customer_number","visible":true},{"id":"date","visible":true},{"id":"due_date","visible":true},{"id":"license_plate","visible":false}]},{"id":"slogan","visible":false,"order":16},{"id":"result","visible":true,"order":5,"fields":[{"id":"result_detail","visible":true},{"id":"result_summary","visible":false}],"boxed":false,"style":{"borderWidth":0,"padding":0}},{"id":"customer","visible":true,"order":2,"column":"left","fields":[{"id":"customer_name","visible":true},{"id":"customer_company","visible":true},{"id":"customer_address","visible":true},{"id":"customer_email","visible":true},{"id":"customer_phone","visible":true},{"id":"customer_tax_id","visible":true}],"boxed":false},{"id":"vehicle","visible":true,"order":3,"column":"left","fields":[{"id":"vehicle_name","visible":true},{"id":"vin","visible":true},{"id":"license_plate","visible":true},{"id":"mileage","visible":true},{"id":"hsn_tsn","visible":true}],"boxed":false},{"id":"test_details","visible":true,"order":4,"column":"right","fields":[{"id":"test_date","visible":true},{"id":"test_location","visible":true},{"id":"inspector","visible":true},{"id":"certificate_number","visible":true},{"id":"vehicle_category","visible":true},{"id":"odometer","visible":true},{"id":"next_test_due","visible":true}],"boxed":false},{"id":"defects","visible":true,"order":7,"fields":[{"id":"defect_notes","visible":true},{"id":"defect_photos","visible":false}]},{"id":"results_table","visible":true,"order":6,"fields":[{"id":"passed_checks","visible":true},{"id":"not_applicable_checks","visible":true},{"id":"check_notes","visible":true},{"id":"combined_table","visible":true}],"style":{"borderWidth":0.5,"outerBorder":true}},{"id":"condition_map","visible":true,"order":8,"fields":[{"id":"legend","visible":true},{"id":"previous_marks","visible":true}]},{"id":"inspection_photos","visible":true,"order":9},{"id":"notes","visible":true,"order":10,"boxed":false},{"id":"attached_documents","visible":true,"order":12,"boxed":false},{"id":"signature","visible":true,"order":11,"fields":[{"id":"signature_image","visible":true},{"id":"inspector_line","visible":true},{"id":"inspector_name","visible":true},{"id":"date_line","visible":true},{"id":"customer_line","visible":false}]},{"id":"footer","visible":true,"order":13,"fields":[{"id":"footer_note","visible":true},{"id":"portal_link","visible":true},{"id":"logo","visible":true},{"id":"company_name","visible":true},{"id":"company_address","visible":true},{"id":"company_phone","visible":true},{"id":"company_email","visible":true},{"id":"bank_account","visible":true},{"id":"company_org_number","visible":true}]}],"document":{"accentColor":"#111827","stripes":false,"rowPadding":3,"fontSize":9},"documentType":"certificate","version":3});
  124. // Copied from src/features/tire-hotel/Lib/tireConstants rather than imported,
  125. // for the same reason as the inspection templates further down: the production
  126. // image ships only prisma/ and src/generated (see Dockerfile), so an import
  127. // reaching into src/features fails the deploy job with MODULE_NOT_FOUND. That
  128. // file stays the source of truth — change it there first, then mirror it here.
  129. /** The four road positions, in the order a technician walks around a car. */
  130. const TIRE_ROAD_POSITIONS = ["front_left", "front_right", "rear_left", "rear_right"] as const;
  131. type SeedTreadThresholds = { summerReplace: number; winterReplace: number; warnMargin: number };
  132. function gradeTread(
  133. treadDepthMm: number | null | undefined,
  134. season: string,
  135. thresholds: SeedTreadThresholds,
  136. ): "good" | "fair" | "replace" | null {
  137. if (treadDepthMm == null) return null;
  138. const limit = season === "winter" ? thresholds.winterReplace : thresholds.summerReplace;
  139. if (treadDepthMm < limit) return "replace";
  140. if (treadDepthMm < limit + thresholds.warnMargin) return "fair";
  141. return "good";
  142. }
  143. const DATA_ROOT = process.env.DATA_ROOT || path.join(process.cwd(), "data");
  144. const UPLOAD_DIR = path.join(DATA_ROOT, "uploads", ORG_ID, "vehicles");
  145. // Image assets bundled with the repo — preferred over live URL downloads so
  146. // the seed never fails when an Unsplash photo gets removed.
  147. const BUNDLED_IMAGE_DIR = path.join(process.cwd(), "prisma", "seed-assets", "vehicles");
  148. // All CDN URLs verified: extracted from Unsplash photo pages via curl, all return HTTP 200
  149. const vehicleImages: Record<string, string> = {
  150. // Cars
  151. "toyota-camry.jpg": "https://images.unsplash.com/photo-1621007947382-bb3c3994e3fb?w=800&q=80",
  152. "ford-f150-white.jpg": "https://images.unsplash.com/photo-1605152322346-bd2391778772?w=800&q=80",
  153. "ford-f150-grey.jpg": "https://images.unsplash.com/photo-1614218110929-caa460524fc1?w=800&q=80",
  154. "ford-f150-black.jpg": "https://images.unsplash.com/photo-1691809158613-1e4d51ad086f?w=800&q=80",
  155. "bmw-3series.jpg": "https://images.unsplash.com/photo-1629994562870-75d504fc02a1?w=800&q=80",
  156. "honda-civic.jpg": "https://images.unsplash.com/photo-1636915873177-a0c1a48d84eb?w=800&q=80",
  157. "tesla-model3.jpg": "https://images.unsplash.com/photo-1610470832703-95d40c3fad55?w=800&q=80",
  158. "tesla-modely.jpg": "https://images.unsplash.com/photo-1504507926084-34cf0b939964?w=800&q=80",
  159. "chevy-silverado.jpg": "https://images.unsplash.com/photo-1601919652576-41bad7c8ccbd?w=800&q=80",
  160. "mercedes-sprinter.jpg": "https://images.unsplash.com/photo-1535655685871-dc8158ff167e?w=800&q=80",
  161. "audi-a4.jpg": "https://images.unsplash.com/photo-1619732569416-83a8adfd8ac4?w=800&q=80",
  162. "jeep-wrangler.jpg": "https://images.unsplash.com/photo-1579097647275-16b002cced9e?w=800&q=80",
  163. "jeep-wrangler-orange.jpg": "https://images.unsplash.com/photo-1622364631585-831e11682a67?w=800&q=80",
  164. "porsche-911.jpg": "https://images.unsplash.com/photo-1624880056652-c7993c99190f?w=800&q=80",
  165. "vw-golf-gti-red.jpg": "https://images.unsplash.com/photo-1560282105-222992ffb774?w=800&q=80",
  166. "vw-golf-gti-grey.jpg": "https://images.unsplash.com/photo-1746184842826-73fd144a4587?w=800&q=80",
  167. "ram-1500.jpg": "https://images.unsplash.com/photo-1649793395985-967862a3b73f?w=800&q=80",
  168. "ram-1500-black.jpg": "https://images.unsplash.com/photo-1678492861610-0bd650a846fe?w=800&q=80",
  169. "toyota-tacoma.jpg": "https://images.unsplash.com/photo-1509510779868-337c9f58ad54?w=800&q=80",
  170. "subaru-outback.jpg": "https://images.unsplash.com/photo-1681680061152-d3fc0b999a8e?w=800&q=80",
  171. "subaru-forester.jpg": "https://images.unsplash.com/photo-1687048988997-ec57f83ea3bd?w=800&q=80",
  172. // Trucks
  173. "kenworth-t680.jpg": "https://images.unsplash.com/photo-1586191552066-d52dd1e3af86?w=800&q=80",
  174. "freightliner-cascadia.jpg": "https://images.unsplash.com/photo-1616432043562-3671ea2e5242?w=800&q=80",
  175. "volvo-fh640.jpg": "https://images.unsplash.com/photo-1633966100013-25a16d17b43d?w=800&q=80",
  176. "mack-granite.jpg": "https://images.unsplash.com/photo-1746349086423-06ea6b4d73f7?w=800&q=80",
  177. "concrete-mixer.jpg": "https://images.unsplash.com/photo-1530139675202-8c52bb810762?w=800&q=80",
  178. "dump-truck.jpg": "https://images.unsplash.com/photo-1686945127938-0296f10937ed?w=800&q=80",
  179. // Tractors
  180. "john-deere-6r.jpg": "https://images.unsplash.com/photo-1604923757148-f6e6957199c3?w=800&q=80",
  181. "fendt-942.jpg": "https://images.unsplash.com/photo-1629818572083-3ceb38e34322?w=800&q=80",
  182. "massey-ferguson.jpg": "https://images.unsplash.com/photo-1635351461882-9ffff1ce323c?w=800&q=80",
  183. "tractor-wheat.jpg": "https://images.unsplash.com/photo-1720273071277-190dd8f0a05a?w=800&q=80",
  184. "tractor-field.jpg": "https://images.unsplash.com/photo-1633554587766-ffc8151e31c0?w=800&q=80",
  185. "combine-harvester.jpg": "https://images.unsplash.com/photo-1754893889385-6c5a2d675524?w=800&q=80",
  186. "combine-jd.jpg": "https://images.unsplash.com/photo-1635174815612-fd9636f70146?w=800&q=80",
  187. // Heavy Machinery
  188. "cat-d6-bulldozer.jpg": "https://images.unsplash.com/photo-1621922688758-359fc864071e?w=800&q=80",
  189. "bulldozer-rocks.jpg": "https://images.unsplash.com/photo-1710669430606-30a8ceeb6e71?w=800&q=80",
  190. "komatsu-excavator.jpg": "https://images.unsplash.com/photo-1649807533255-bbc9c9fb7d77?w=800&q=80",
  191. "excavator-soil.jpg": "https://images.unsplash.com/photo-1622082679766-c5912d9416eb?w=800&q=80",
  192. "volvo-ec220e.jpg": "https://images.unsplash.com/photo-1568678453977-a90de6812e7a?w=800&q=80",
  193. "mini-excavator.jpg": "https://images.unsplash.com/photo-1722925837890-0fa0f0c9a356?w=800&q=80",
  194. "wheel-loader.jpg": "https://images.unsplash.com/photo-1630288214032-2c4cc2c080ca?w=800&q=80",
  195. "bulldozer-road.jpg": "https://images.unsplash.com/photo-1684431438566-6ff4a0df9190?w=800&q=80",
  196. "loader-field.jpg": "https://images.unsplash.com/photo-1727863526509-7670e063f308?w=800&q=80",
  197. "liebherr-crane.jpg": "https://images.unsplash.com/photo-1685305934074-3a7c37614c29?w=800&q=80",
  198. "crane-yellow.jpg": "https://images.unsplash.com/photo-1725882176928-de328f0d485a?w=800&q=80",
  199. "forklift.jpg": "https://images.unsplash.com/photo-1740914994657-f1cdffdc418e?w=800&q=80",
  200. // Additional vehicles
  201. "corvette-c8.jpg": "https://images.unsplash.com/photo-1750050677183-4e68b1d21b65?w=800&q=80",
  202. "land-rover-defender.jpg": "https://images.unsplash.com/photo-1741905358583-1999b08d6e2e?w=800&q=80",
  203. "dodge-charger.jpg": "https://images.unsplash.com/photo-1674520669939-1a8dbcdc2f6b?w=800&q=80",
  204. "subaru-wrx.jpg": "https://images.unsplash.com/photo-1747000586906-c451da789b62?w=800&q=80",
  205. };
  206. // ── Inspection templates ─────────────────────────────────────────────────
  207. // Deliberately duplicated rather than imported from
  208. // src/features/inspections/Lib/templatePresets: the production image ships only
  209. // prisma/ and src/generated (see Dockerfile), so anything the seed imports from
  210. // src/features breaks the deploy job with MODULE_NOT_FOUND.
  211. //
  212. // Three of these are the built-in library's own checklists, copied from what
  213. // presetToTemplateCreate() writes in English: same sections, checks, limits
  214. // and the condition-map check, plus the provenance (packageId, packageVersion,
  215. // packageSource). syncPresetLibrary() identifies an installed preset by that
  216. // packageId, so the templates page recognises these as installed and adds only
  217. // the presets the seed does not carry, instead of a second "Standard
  218. // multi-point inspection" next to this one. Keep them in step with the
  219. // preset: change templatePresets.ts and inspectionLibrary.json first.
  220. //
  221. // The periodic-inspection checklist is the workshop's own (no packageId). It
  222. // is a subset of the EU preset, codes and limit values included - the common
  223. // Union figures from Annex I of Directive 2014/45/EU. Do not invent codes or
  224. // thresholds here; copy them from the preset, which is where they are
  225. // maintained.
  226. type SeedItem = {
  227. name: string;
  228. code?: string;
  229. description?: string;
  230. inputType?: string;
  231. unit?: string;
  232. minValue?: number;
  233. maxValue?: number;
  234. choices?: string[];
  235. required?: boolean;
  236. photoRequired?: boolean;
  237. allowNotApplicable?: boolean;
  238. defaultSeverity?: string;
  239. };
  240. type SeedSection = { name: string; code?: string; description?: string; items: SeedItem[] };
  241. type SeedTemplate = {
  242. name: string;
  243. description: string;
  244. standard: string;
  245. country: string | null;
  246. severityScale: string;
  247. /** Set on the library's own checklists, exactly as presetToTemplateCreate() records them. */
  248. packageId?: string;
  249. packageVersion?: string;
  250. packageSource?: string;
  251. sections: SeedSection[];
  252. };
  253. /** The package ids the library gives its presets (templatePresets presetPackageId). */
  254. const PRESET_MULTIPOINT = "torqvoice/standard-multipoint";
  255. const PRESET_PRE_PURCHASE = "torqvoice/pre-purchase";
  256. const PRESET_VEHICLE_INTAKE = "torqvoice/vehicle-intake";
  257. const SEED_TEMPLATES: SeedTemplate[] = [
  258. {
  259. name: "Standard multi-point inspection",
  260. description: "A general workshop health check covering the major systems. Graded pass / attention / fail rather than on the EU defect scale.",
  261. standard: "custom",
  262. country: null,
  263. severityScale: "basic",
  264. packageId: "torqvoice/standard-multipoint",
  265. packageVersion: "1.0.0",
  266. packageSource: "builtin",
  267. sections: [
  268. {
  269. name: "Exterior",
  270. items: [
  271. { name: "Body condition", description: "Tap the drawing where each dent, scratch or chip is, and add a note and a photo. Marks from earlier visits show in grey.", inputType: "condition_map" },
  272. { name: "Paint" },
  273. { name: "Lights" },
  274. { name: "Windscreen" },
  275. { name: "Wipers" },
  276. { name: "Mirrors" },
  277. ],
  278. },
  279. {
  280. name: "Under hood",
  281. items: [
  282. { name: "Engine oil level and condition" },
  283. { name: "Coolant", inputType: "measurement", unit: "°C", maxValue: -20, defaultSeverity: "attention" },
  284. { name: "Brake fluid" },
  285. { name: "Power steering fluid" },
  286. { name: "Battery voltage", inputType: "measurement", unit: "V", minValue: 12.4, defaultSeverity: "attention" },
  287. { name: "Drive belts" },
  288. { name: "Hoses" },
  289. { name: "Air filter" },
  290. { name: "Cabin filter" },
  291. ],
  292. },
  293. {
  294. name: "Under vehicle",
  295. items: [
  296. { name: "Exhaust system" },
  297. { name: "Suspension" },
  298. { name: "CV joints and boots" },
  299. { name: "Brake lines" },
  300. { name: "Fluid leaks" },
  301. ],
  302. },
  303. {
  304. name: "Brakes",
  305. items: [
  306. { name: "Front pads", inputType: "measurement", unit: "mm", minValue: 3, defaultSeverity: "fail" },
  307. { name: "Rear pads", inputType: "measurement", unit: "mm", minValue: 3, defaultSeverity: "fail" },
  308. { name: "Discs" },
  309. { name: "Parking brake" },
  310. ],
  311. },
  312. {
  313. name: "Tyres",
  314. items: [
  315. { name: "Tread depth — front left", inputType: "measurement", unit: "mm", minValue: 1.6, defaultSeverity: "fail" },
  316. { name: "Tread depth — front right", inputType: "measurement", unit: "mm", minValue: 1.6, defaultSeverity: "fail" },
  317. { name: "Tread depth — rear left", inputType: "measurement", unit: "mm", minValue: 1.6, defaultSeverity: "fail" },
  318. { name: "Tread depth — rear right", inputType: "measurement", unit: "mm", minValue: 1.6, defaultSeverity: "fail" },
  319. { name: "Tyre pressure", inputType: "measurement", unit: "bar", minValue: 1.8, maxValue: 3.5, defaultSeverity: "attention" },
  320. { name: "Spare tyre or repair kit" },
  321. ],
  322. },
  323. {
  324. name: "Interior",
  325. items: [
  326. { name: "Warning lights" },
  327. { name: "Horn" },
  328. { name: "Air conditioning" },
  329. { name: "Heater" },
  330. { name: "Seat belts" },
  331. ],
  332. },
  333. ],
  334. },
  335. {
  336. name: "Periodic technical inspection",
  337. description:
  338. "Subset of the Annex I checklist from Directive 2014/45/EU, graded on the minor / major / dangerous defect scale. Thresholds are the common Union figures - adjust them to your national test.",
  339. standard: "eu-2014-45",
  340. country: null,
  341. severityScale: "eu",
  342. sections: [
  343. {
  344. name: "Identification of the vehicle",
  345. code: "0",
  346. description: "Annex I, item 0. Confirms the vehicle presented matches its papers.",
  347. items: [
  348. { name: "Registration plates", code: "0.1", required: true },
  349. { name: "Vehicle identification / chassis number", code: "0.2", description: "VIN present, legible and matching the registration document.", required: true },
  350. ],
  351. },
  352. {
  353. name: "Braking equipment",
  354. code: "1",
  355. description: "Annex I, item 1. Mechanical condition, then measured performance.",
  356. items: [
  357. { name: "Service brake pedal / hand lever pivot", code: "1.1.1" },
  358. { name: "Pedal / lever condition and travel", code: "1.1.2" },
  359. { name: "Brake servo unit and master cylinder", code: "1.1.10" },
  360. { name: "Rigid brake pipes", code: "1.1.11" },
  361. { name: "Flexible brake hoses", code: "1.1.12" },
  362. { name: "Brake linings and pads", code: "1.1.13", description: "Remaining friction material at the thinnest point.", inputType: "measurement", unit: "mm", minValue: 3, defaultSeverity: "fail" },
  363. { name: "Brake drums and discs", code: "1.1.14" },
  364. { name: "Complete braking system", code: "1.1.21" },
  365. { name: "Service brake efficiency", code: "1.2.2", description: "Braking rate as a percentage of the maximum authorised mass.", inputType: "measurement", unit: "%", minValue: 50, required: true, defaultSeverity: "fail" },
  366. { name: "Service brake imbalance across an axle", code: "1.2.1", inputType: "measurement", unit: "%", maxValue: 30, defaultSeverity: "fail" },
  367. { name: "Parking brake efficiency", code: "1.4.2", inputType: "measurement", unit: "%", minValue: 16, required: true, defaultSeverity: "fail" },
  368. { name: "Anti-lock braking system (ABS)", code: "1.6" },
  369. { name: "Brake fluid", code: "1.8", description: "Contamination and boiling point." },
  370. ],
  371. },
  372. {
  373. name: "Steering",
  374. code: "2",
  375. description: "Annex I, item 2.",
  376. items: [
  377. { name: "Mechanical condition of the steering", code: "2.1" },
  378. { name: "Steering wheel, column and handlebars", code: "2.2" },
  379. { name: "Steering play", code: "2.3", description: "Free movement at the rim before the road wheels respond.", inputType: "measurement", unit: "mm", maxValue: 120, defaultSeverity: "fail" },
  380. { name: "Wheel alignment", code: "2.4" },
  381. { name: "Power steering", code: "2.6" },
  382. ],
  383. },
  384. {
  385. name: "Visibility",
  386. code: "3",
  387. description: "Annex I, item 3.",
  388. items: [
  389. { name: "Field of vision", code: "3.1" },
  390. { name: "Condition of the glass", code: "3.2", description: "Chips and cracks in the swept area of the windscreen." },
  391. { name: "Rear-view mirrors or devices", code: "3.3" },
  392. { name: "Windscreen wipers", code: "3.4" },
  393. { name: "Windscreen washers", code: "3.5" },
  394. ],
  395. },
  396. {
  397. name: "Lamps, reflectors and electrical equipment",
  398. code: "4",
  399. description: "Annex I, item 4.",
  400. items: [
  401. { name: "Headlamps - condition and operation", code: "4.1.1" },
  402. { name: "Headlamp aim", code: "4.1.2", description: "Downward inclination of the dipped beam.", inputType: "measurement", unit: "%", minValue: -2.5, maxValue: -0.5, defaultSeverity: "attention" },
  403. { name: "Position, side marker and daytime running lamps", code: "4.2" },
  404. { name: "Stop lamps", code: "4.3", required: true },
  405. { name: "Direction indicator and hazard warning lamps", code: "4.4" },
  406. { name: "Rear registration plate lamp", code: "4.7" },
  407. { name: "Electrical wiring", code: "4.11" },
  408. { name: "Battery", code: "4.13" },
  409. ],
  410. },
  411. {
  412. name: "Axles, wheels, tires and suspension",
  413. code: "5",
  414. description: "Annex I, item 5. Tread depth is recorded per wheel.",
  415. items: [
  416. { name: "Axles", code: "5.1.1" },
  417. { name: "Stub axles and wheel bearings", code: "5.1.3" },
  418. { name: "Wheels", code: "5.2.1" },
  419. { name: "Tire condition - sidewalls, load and speed rating", code: "5.2.3" },
  420. { name: "Tread depth - front left", code: "5.2.3", inputType: "measurement", unit: "mm", minValue: 1.6, required: true, defaultSeverity: "fail" },
  421. { name: "Tread depth - front right", code: "5.2.3", inputType: "measurement", unit: "mm", minValue: 1.6, required: true, defaultSeverity: "fail" },
  422. { name: "Tread depth - rear left", code: "5.2.3", inputType: "measurement", unit: "mm", minValue: 1.6, required: true, defaultSeverity: "fail" },
  423. { name: "Tread depth - rear right", code: "5.2.3", inputType: "measurement", unit: "mm", minValue: 1.6, required: true, defaultSeverity: "fail" },
  424. { name: "Springs and stabiliser", code: "5.3.1" },
  425. { name: "Shock absorbers", code: "5.3.2" },
  426. { name: "Suspension arms, rods and joints", code: "5.3.4" },
  427. ],
  428. },
  429. {
  430. name: "Chassis and chassis attachments",
  431. code: "6",
  432. description: "Annex I, item 6.",
  433. items: [
  434. { name: "Chassis or frame and attachments", code: "6.1.1" },
  435. { name: "Exhaust pipes and silencer", code: "6.1.2" },
  436. { name: "Fuel tank and pipes", code: "6.1.3" },
  437. { name: "Coupling device and towing equipment", code: "6.1.7" },
  438. { name: "Cab and bodywork condition", code: "6.2.1" },
  439. { name: "Doors and door catches", code: "6.2.3" },
  440. { name: "Driver seat and seats", code: "6.2.5" },
  441. { name: "Mudguards and spray suppression", code: "6.2.11" },
  442. ],
  443. },
  444. {
  445. name: "Other equipment",
  446. code: "7",
  447. description: "Annex I, item 7. Some checks apply only to certain vehicle categories.",
  448. items: [
  449. { name: "Safety belts, buckles and restraint systems", code: "7.1", required: true },
  450. { name: "Locks and anti-theft device", code: "7.3" },
  451. { name: "Audible warning device", code: "7.7" },
  452. { name: "Speedometer", code: "7.8" },
  453. { name: "Odometer reading", code: "7.11", description: "Reading at the time of the test. Annex IV requires this on the certificate.", inputType: "measurement", unit: "km", required: true },
  454. ],
  455. },
  456. ],
  457. },
  458. {
  459. name: "Pre-purchase inspection",
  460. description: "Buyer-facing report covering documentation, body, mechanicals and wear items, with room for measurements and photos.",
  461. standard: "custom",
  462. country: null,
  463. severityScale: "basic",
  464. packageId: "torqvoice/pre-purchase",
  465. packageVersion: "1.0.0",
  466. packageSource: "builtin",
  467. sections: [
  468. {
  469. name: "Documentation and history",
  470. items: [
  471. { name: "Registration document matches the vehicle", required: true },
  472. { name: "VIN", inputType: "text", required: true },
  473. { name: "Service history", inputType: "choice", choices: ["Full", "Partial", "None"] },
  474. { name: "Outstanding recalls" },
  475. { name: "Previous accident damage" },
  476. ],
  477. },
  478. {
  479. name: "Body and paint",
  480. items: [
  481. { name: "Body condition", description: "Tap the drawing where each dent, scratch or chip is, and add a note and a photo. Marks from earlier visits show in grey.", inputType: "condition_map" },
  482. { name: "Panel gaps" },
  483. { name: "Paint thickness — worst panel", inputType: "measurement", unit: "µm", minValue: 80, maxValue: 200, defaultSeverity: "attention" },
  484. { name: "Corrosion", photoRequired: true },
  485. { name: "Glass and lights" },
  486. ],
  487. },
  488. {
  489. name: "Mechanical",
  490. items: [
  491. { name: "Cold start" },
  492. { name: "Engine noise" },
  493. { name: "Compression — lowest cylinder", inputType: "measurement", unit: "bar", minValue: 10, defaultSeverity: "fail" },
  494. { name: "Gearbox and clutch" },
  495. { name: "Fault codes on the diagnostic port", inputType: "text" },
  496. { name: "Road test" },
  497. ],
  498. },
  499. {
  500. name: "Wear items",
  501. items: [
  502. { name: "Brake pads and discs" },
  503. { name: "Tyres including date codes" },
  504. { name: "Suspension bushes" },
  505. { name: "Exhaust" },
  506. ],
  507. },
  508. ],
  509. },
  510. {
  511. name: "Vehicle check-in",
  512. description: "The walk-round at drop-off, done with the customer standing there: how the car arrived, what was already on the body, what was left inside. Prints on the work order the customer signs.",
  513. standard: "custom",
  514. country: null,
  515. severityScale: "basic",
  516. packageId: "torqvoice/vehicle-intake",
  517. packageVersion: "1.0.0",
  518. packageSource: "builtin",
  519. sections: [
  520. {
  521. name: "Arrival",
  522. items: [
  523. { name: "Fuel level", inputType: "choice", choices: ["Empty", "1/4", "1/2", "3/4", "Full"] },
  524. { name: "Keys handed over", inputType: "text" },
  525. { name: "Warning lights on arrival", inputType: "text" },
  526. { name: "Personal belongings left in the vehicle", inputType: "text" },
  527. ],
  528. },
  529. {
  530. name: "Exterior walk-round",
  531. items: [
  532. { name: "Body condition", description: "Tap the drawing where each dent, scratch or chip is, and add a note and a photo. Marks from earlier visits show in grey.", inputType: "condition_map", required: true },
  533. { name: "Glass condition" },
  534. { name: "Lights" },
  535. { name: "Mirrors" },
  536. { name: "Wheels and tyres, visual" },
  537. ],
  538. },
  539. {
  540. name: "Interior",
  541. items: [
  542. { name: "Seats and trim" },
  543. { name: "Floor and mats" },
  544. { name: "Infotainment and controls" },
  545. ],
  546. },
  547. ],
  548. },
  549. ];
  550. function downloadFile(url: string, dest: string): Promise<void> {
  551. return new Promise((resolve, reject) => {
  552. const dir = path.dirname(dest);
  553. if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
  554. const follow = (u: string, r = 0) => {
  555. if (r > 5) return reject(new Error("Too many redirects"));
  556. const mod = u.startsWith("https") ? https : http;
  557. mod.get(u, { headers: { "User-Agent": "Seed/1.0" } }, (res) => {
  558. if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) return follow(res.headers.location, r + 1);
  559. if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode}`));
  560. const f = fs.createWriteStream(dest);
  561. res.pipe(f);
  562. f.on("finish", () => { f.close(); resolve(); });
  563. f.on("error", (e) => { fs.unlinkSync(dest); reject(e); });
  564. }).on("error", reject);
  565. };
  566. follow(url);
  567. });
  568. }
  569. function img(cat: string, file: string) { return `/api/files/${ORG_ID}/${cat}/${file}`; }
  570. /**
  571. * A drop-off or inspection photo, made from the cached stock photo of the
  572. * vehicle (the same file its vehicle page shows). The seed ships no photos
  573. * of its own, so this is the only picture it has of each car. Copied under a
  574. * fixed name into the folder the app files such uploads in, which
  575. * cleanupUploads() empties and this refills on every reset. Null when the
  576. * stock photo never downloaded; the caller then seeds no attachment.
  577. */
  578. function seedPhoto(vehicleFile: string, folder: "services" | "inspections", name: string) {
  579. const source = path.join(UPLOAD_DIR, vehicleFile);
  580. if (!fs.existsSync(source)) return null;
  581. const dir = path.join(DATA_ROOT, "uploads", ORG_ID, folder);
  582. fs.mkdirSync(dir, { recursive: true });
  583. fs.copyFileSync(source, path.join(dir, name));
  584. return {
  585. fileName: name,
  586. fileUrl: `/api/protected/files/${ORG_ID}/${folder}/${name}`,
  587. fileType: "image/jpeg",
  588. fileSize: fs.statSync(source).size,
  589. };
  590. }
  591. /**
  592. * A handwritten-looking signature as a PNG, for the demo owner's saved
  593. * signature. Drawn here from a formula rather than shipped as a file, with
  594. * node's zlib only, because the image may import nothing but the generated
  595. * client and node built-ins. Transparent background, dark blue ink.
  596. */
  597. function signaturePng(): Buffer {
  598. const width = 420;
  599. const height = 130;
  600. const pixels = Buffer.alloc(width * height * 4);
  601. const ink = [23, 37, 84];
  602. const dab = (cx: number, cy: number, r: number) => {
  603. for (let y = Math.floor(cy - r - 1); y <= Math.ceil(cy + r + 1); y++) {
  604. for (let x = Math.floor(cx - r - 1); x <= Math.ceil(cx + r + 1); x++) {
  605. if (x < 0 || y < 0 || x >= width || y >= height) continue;
  606. // Anti-aliased edge: full ink inside the radius, fading over a pixel.
  607. const coverage = Math.max(0, Math.min(1, r + 0.5 - Math.hypot(x - cx, y - cy)));
  608. if (coverage === 0) continue;
  609. const i = (y * width + x) * 4;
  610. const alpha = Math.max(pixels[i + 3], Math.round(coverage * 255));
  611. pixels[i] = ink[0];
  612. pixels[i + 1] = ink[1];
  613. pixels[i + 2] = ink[2];
  614. pixels[i + 3] = alpha;
  615. }
  616. }
  617. };
  618. // A slanted, looping stroke: a capital, a run of joined loops that shrink
  619. // like lowercase letters, then a long underline flourish.
  620. const stroke = (points: (t: number) => [number, number], steps: number, width0: number) => {
  621. for (let s = 0; s <= steps; s++) {
  622. const t = s / steps;
  623. const [x, y] = points(t);
  624. dab(x, y, width0 * (0.75 + 0.35 * Math.sin(t * Math.PI)));
  625. }
  626. };
  627. // Capital D: a slanted stem, then the bowl drawn round from its top.
  628. stroke((t) => [52 - 10 * t, 30 + 66 * t], 400, 2.2);
  629. stroke((t) => {
  630. const a = -Math.PI / 2 + t * Math.PI * 1.15;
  631. return [42 + 6 * (1 - t) + 48 * Math.cos(a), 63 + 33 * Math.sin(a)];
  632. }, 900, 2.1);
  633. // The rest of the name: joined loops of uneven height, tailing off.
  634. stroke((t) => {
  635. const a = t * Math.PI * 11;
  636. const height = 16 * (0.8 + 0.45 * Math.sin(t * 9.4)) * (1 - 0.4 * t);
  637. return [98 + 240 * t + 10 * Math.cos(a), 72 - height * Math.sin(a) - 6 * t];
  638. }, 3000, 1.8);
  639. stroke((t) => [70 + 300 * t, 104 - 10 * Math.sin(t * Math.PI) + 4 * t], 1200, 1.6);
  640. const crcTable = Array.from({ length: 256 }, (_, n) => {
  641. let c = n;
  642. for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
  643. return c >>> 0;
  644. });
  645. const crc32 = (buf: Buffer) => {
  646. let c = 0xffffffff;
  647. for (const b of buf) c = crcTable[(c ^ b) & 0xff] ^ (c >>> 8);
  648. return (c ^ 0xffffffff) >>> 0;
  649. };
  650. const chunk = (type: string, data: Buffer) => {
  651. const len = Buffer.alloc(4);
  652. len.writeUInt32BE(data.length);
  653. const body = Buffer.concat([Buffer.from(type, "latin1"), data]);
  654. const crc = Buffer.alloc(4);
  655. crc.writeUInt32BE(crc32(body));
  656. return Buffer.concat([len, body, crc]);
  657. };
  658. const header = Buffer.alloc(13);
  659. header.writeUInt32BE(width, 0);
  660. header.writeUInt32BE(height, 4);
  661. header[8] = 8; // bit depth
  662. header[9] = 6; // RGBA
  663. const rows = Buffer.alloc((width * 4 + 1) * height);
  664. for (let y = 0; y < height; y++) {
  665. rows[y * (width * 4 + 1)] = 0; // no filter
  666. pixels.copy(rows, y * (width * 4 + 1) + 1, y * width * 4, (y + 1) * width * 4);
  667. }
  668. return Buffer.concat([
  669. Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
  670. chunk("IHDR", header),
  671. chunk("IDAT", deflateSync(rows)),
  672. chunk("IEND", Buffer.alloc(0)),
  673. ]);
  674. }
  675. async function provisionDemoAccount() {
  676. console.log("Provisioning demo user + organization...");
  677. // Owner user (idempotent — safe to re-run)
  678. await prisma.user.upsert({
  679. where: { id: USER_ID },
  680. create: {
  681. id: USER_ID,
  682. email: DEMO_EMAIL,
  683. name: "Demo Owner",
  684. emailVerified: true,
  685. termsAcceptedAt: new Date(),
  686. },
  687. update: { email: DEMO_EMAIL, name: "Demo Owner" },
  688. });
  689. // Credential account — wipe + recreate so the password always matches env
  690. const hashedPassword = hashPassword(DEMO_PASSWORD);
  691. await prisma.account.deleteMany({ where: { userId: USER_ID, providerId: "credential" } });
  692. await prisma.account.create({
  693. data: { accountId: USER_ID, providerId: "credential", userId: USER_ID, password: hashedPassword },
  694. });
  695. // Organization
  696. await prisma.organization.upsert({
  697. where: { id: ORG_ID },
  698. create: { id: ORG_ID, name: DEMO_ORG_NAME, portalSlug: "demo-workshop" },
  699. update: { name: DEMO_ORG_NAME, portalSlug: "demo-workshop" },
  700. });
  701. // Belt and braces alongside the demoGuard on org creation: purge any
  702. // foreign organizations so the reset always converges on a single org.
  703. // Org-scoped rows go with them via ON DELETE CASCADE, and stale
  704. // active-org cookies fall back to the demo org membership.
  705. //
  706. // RESTRICT foreign keys are checked row by row inside the cascade, so a
  707. // parent and the row pointing at it cannot go in the same statement. The
  708. // schema has five:
  709. // inspections.templateId -> inspection_templates
  710. // service_records.issuedDesignSnapshotId -> document_design_snapshots
  711. // service_records.issuedLogoSnapshotId / issuedSignatureSnapshotId
  712. // -> document_asset_snapshots (an issued invoice freezes these)
  713. // import_batches.userId -> users (cleanup() handles it before users go)
  714. // subscriptions.planId -> subscription_plans (global plans, never purged)
  715. // so the inspections and work orders of those organizations go first.
  716. await prisma.inspection.deleteMany({
  717. where: { organizationId: { not: ORG_ID } },
  718. });
  719. await prisma.serviceRecord.deleteMany({
  720. where: { organizationId: { not: ORG_ID } },
  721. });
  722. const foreignOrgs = await prisma.organization.deleteMany({
  723. where: { id: { not: ORG_ID } },
  724. });
  725. if (foreignOrgs.count > 0) {
  726. console.log(` Removed ${foreignOrgs.count} visitor-created organization(s).`);
  727. }
  728. // Owner membership
  729. await prisma.organizationMember.upsert({
  730. where: { userId_organizationId: { userId: USER_ID, organizationId: ORG_ID } },
  731. create: { userId: USER_ID, organizationId: ORG_ID, role: "owner" },
  732. update: { role: "owner" },
  733. });
  734. console.log(` Owner: ${DEMO_EMAIL} (org: ${DEMO_ORG_NAME})\n`);
  735. }
  736. /**
  737. * Remove files a demo visitor uploaded, while preserving the seed's own cached
  738. * vehicle images. `prisma/seed-assets/vehicles/` ships empty, so those images
  739. * were fetched from Unsplash once and cached in the data volume — deleting them
  740. * would trigger a full re-download on every 3-hourly reset.
  741. */
  742. function cleanupUploads() {
  743. const orgRoot = path.join(DATA_ROOT, "uploads", ORG_ID);
  744. if (!fs.existsSync(orgRoot)) return;
  745. const keep = new Set(Object.keys(vehicleImages));
  746. let removed = 0;
  747. const walk = (dir: string) => {
  748. for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
  749. const full = path.join(dir, entry.name);
  750. if (entry.isDirectory()) {
  751. walk(full);
  752. // Drop directories that are now empty (e.g. documents/, diagnostics/).
  753. if (fs.readdirSync(full).length === 0) fs.rmdirSync(full);
  754. continue;
  755. }
  756. // Preserve the cached seed images living in uploads/<org>/vehicles/.
  757. if (dir === UPLOAD_DIR && keep.has(entry.name)) continue;
  758. fs.unlinkSync(full);
  759. removed++;
  760. }
  761. };
  762. try {
  763. walk(orgRoot);
  764. console.log(` Removed ${removed} visitor-uploaded file(s).`);
  765. } catch (err) {
  766. console.warn(" Upload cleanup failed:", err);
  767. }
  768. cleanupTrash(keep);
  769. }
  770. /**
  771. * Empty this workshop's share of the uploads trash.
  772. *
  773. * The app's file manager (src/lib/files/manager.ts, not importable here) never
  774. * deletes a released upload at once: it moves it to
  775. * `<uploads>/.trash/<YYYY-MM-DD>/<org>/<folder>/<name>` and purges it 30 days
  776. * later. Everything a visitor removed between resets therefore sits there,
  777. * visitor photos included, and would outlive the reset by a month. Only the
  778. * demo org's folder under each day is touched; the layout is the manager's.
  779. *
  780. * A seed vehicle photo a visitor released (by deleting the vehicle) is moved
  781. * back to uploads/<org>/vehicles/ first, which saves downloading it again.
  782. */
  783. function cleanupTrash(keep: Set<string>) {
  784. // The same roots the app reads: DATA_ROOT's uploads, then the old default.
  785. const roots = [...new Set([path.join(DATA_ROOT, "uploads"), path.join(process.cwd(), "data", "uploads")])];
  786. let removed = 0;
  787. for (const root of roots) {
  788. const trash = path.join(root, ".trash");
  789. if (!fs.existsSync(trash)) continue;
  790. try {
  791. for (const day of fs.readdirSync(trash, { withFileTypes: true })) {
  792. if (!day.isDirectory() || !/^\d{4}-\d{2}-\d{2}$/.test(day.name)) continue;
  793. const orgTrash = path.join(trash, day.name, ORG_ID);
  794. if (!fs.existsSync(orgTrash)) continue;
  795. const trashedVehicles = path.join(orgTrash, "vehicles");
  796. if (fs.existsSync(trashedVehicles)) {
  797. for (const name of fs.readdirSync(trashedVehicles)) {
  798. const dest = path.join(UPLOAD_DIR, name);
  799. if (keep.has(name) && !fs.existsSync(dest)) {
  800. fs.mkdirSync(UPLOAD_DIR, { recursive: true });
  801. fs.copyFileSync(path.join(trashedVehicles, name), dest);
  802. }
  803. }
  804. }
  805. removed += fs.readdirSync(orgTrash, { recursive: true }).length;
  806. fs.rmSync(orgTrash, { recursive: true, force: true });
  807. // The day folder goes too once nothing of another workshop is in it.
  808. if (fs.readdirSync(path.join(trash, day.name)).length === 0) fs.rmdirSync(path.join(trash, day.name));
  809. }
  810. } catch (err) {
  811. console.warn(" Trash cleanup failed:", err);
  812. }
  813. }
  814. if (removed > 0) console.log(` Emptied ${removed} trashed item(s) of the demo workshop.`);
  815. }
  816. async function cleanup() {
  817. console.log("Cleaning existing data...");
  818. // Accounts a visitor created inside the demo: a technician given the app
  819. // (createTechnicianAccount), a member added some other way. Collected before
  820. // the memberships and technicians that point at them are deleted below.
  821. const visitorUsers = await prisma.user.findMany({
  822. where: {
  823. id: { not: USER_ID },
  824. isSuperAdmin: false,
  825. OR: [
  826. { organizationMembers: { some: { organizationId: ORG_ID } } },
  827. { technicians: { some: { organizationId: ORG_ID } } },
  828. { appSetupCodes: { some: { organizationId: ORG_ID } } },
  829. { pushDevices: { some: { organizationId: ORG_ID } } },
  830. ],
  831. },
  832. select: { id: true },
  833. });
  834. const visitorUserIds = visitorUsers.map((u) => u.id);
  835. // Config/state a visitor can change that would otherwise survive every reset:
  836. // branding, invoice templates, webhooks pointing at external URLs, schedules,
  837. // custom presets. Deleting a Webhook cascades its WebhookDelivery rows.
  838. await prisma.webhook.deleteMany({ where: { organizationId: ORG_ID } });
  839. await prisma.reportSchedule.deleteMany({ where: { organizationId: ORG_ID } });
  840. // Customer and vehicle are both SetNull here, so these outlive the core
  841. // wipe below and have to go explicitly.
  842. await prisma.scheduledMessage.deleteMany({ where: { organizationId: ORG_ID } });
  843. // organizationId is a plain column on these two, so no cascade reaches them.
  844. await prisma.dashboardWidget.deleteMany({ where: { organizationId: ORG_ID } });
  845. await prisma.serviceRequest.deleteMany({ where: { organizationId: ORG_ID } });
  846. await prisma.teamInvitation.deleteMany({ where: { organizationId: ORG_ID } });
  847. await prisma.laborPreset.deleteMany({ where: { organizationId: ORG_ID } });
  848. // Inspection.template is a required relation with no onDelete, so Postgres
  849. // restricts the template delete while any inspection still points at it.
  850. // Vehicle deletion further down would cascade these away, but that runs too
  851. // late - drop them here first. InspectionItem, InspectionAttachment,
  852. // InspectionQuoteRequest and their status reports cascade from Inspection;
  853. // Quote.inspectionId, ServiceRecord.inspectionId and a condition mark's
  854. // inspection link are SetNull.
  855. await prisma.inspectionQuoteRequest.deleteMany({ where: { organizationId: ORG_ID } });
  856. await prisma.inspection.deleteMany({ where: { organizationId: ORG_ID } });
  857. await prisma.inspectionTemplate.deleteMany({ where: { organizationId: ORG_ID } });
  858. await prisma.customFieldDefinition.deleteMany({ where: { organizationId: ORG_ID } });
  859. await prisma.subscription.deleteMany({ where: { organizationId: ORG_ID } });
  860. // Wipes branding, logo, invoice layout, localization, tax config. The
  861. // maintenance.* keys are re-upserted later in the seed.
  862. await prisma.appSetting.deleteMany({ where: { organizationId: ORG_ID } });
  863. // Links, jobs, logs and external calendar events cascade. Recreated below
  864. // from the placeholder provider config, so the catalog shows the same three
  865. // vendors connected.
  866. await prisma.integrationConnection.deleteMany({ where: { organizationId: ORG_ID } });
  867. // The workshop's own mail templates, work order statuses and condition-map
  868. // marks and kinds. Only the organization cascades to these, which the
  869. // reset never deletes.
  870. await prisma.emailTemplate.deleteMany({ where: { organizationId: ORG_ID } });
  871. await prisma.workOrderStatus.deleteMany({ where: { organizationId: ORG_ID } });
  872. await prisma.conditionMark.deleteMany({ where: { organizationId: ORG_ID } });
  873. await prisma.conditionMarkType.deleteMany({ where: { organizationId: ORG_ID } });
  874. // Spreadsheet imports. Rows they created point back with SetNull; a batch
  875. // also holds a RESTRICT key on its user, so it goes before any visitor
  876. // account below.
  877. await prisma.importBatch.deleteMany({
  878. where: { OR: [{ organizationId: ORG_ID }, { userId: { in: visitorUserIds } }] },
  879. });
  880. // Inspection reminder campaigns; their sends cascade from the campaign.
  881. await prisma.inspectionReminderCampaign.deleteMany({ where: { organizationId: ORG_ID } });
  882. await prisma.vehicleInspectionStatus.deleteMany({ where: { organizationId: ORG_ID } });
  883. // Transient/accumulating records.
  884. await prisma.aiChat.deleteMany({ where: { organizationId: ORG_ID } });
  885. await prisma.notification.deleteMany({ where: { organizationId: ORG_ID } });
  886. await prisma.telegramMessage.deleteMany({ where: { organizationId: ORG_ID } });
  887. // Customer is SetNull here, so these survive the customer wipe further down
  888. // and every reset would stack another set of conversations on the inbox.
  889. await prisma.whatsappMessage.deleteMany({ where: { organizationId: ORG_ID } });
  890. await prisma.customerMagicLink.deleteMany({ where: { organizationId: ORG_ID } });
  891. await prisma.customerSmsCode.deleteMany({ where: { organizationId: ORG_ID } });
  892. await prisma.auditLog.deleteMany({ where: { organizationId: ORG_ID } });
  893. // The demo owner's saved signature hangs off the membership, which the
  894. // reset keeps, so it would otherwise outlive every reset. Reseeded below.
  895. await prisma.memberSignature.deleteMany({ where: { member: { organizationId: ORG_ID } } });
  896. // Any team member a visitor added - but never the demo owner, which
  897. // provisionDemoAccount() upserts just before this runs.
  898. await prisma.organizationMember.deleteMany({
  899. where: { organizationId: ORG_ID, userId: { not: USER_ID } },
  900. });
  901. // Custom roles; OrganizationMember.roleId is onDelete: SetNull, so safe.
  902. await prisma.role.deleteMany({ where: { organizationId: ORG_ID } });
  903. // Technician app: sign-in codes, setup codes and phones registered for
  904. // push, plus the job clock. All org-scoped, so they go by organization
  905. // rather than through whoever they belong to.
  906. await prisma.technicianLoginCode.deleteMany({ where: { organizationId: ORG_ID } });
  907. await prisma.technicianSetupCode.deleteMany({ where: { organizationId: ORG_ID } });
  908. await prisma.pushDevice.deleteMany({ where: { organizationId: ORG_ID } });
  909. await prisma.timeEntry.deleteMany({ where: { organizationId: ORG_ID } });
  910. // Core CRUD data. CustomerSession cascades from Customer; ServiceConcern,
  911. // ServiceAttachment, Payment, parts and labor from ServiceRecord.
  912. // Inspections are already gone above.
  913. await prisma.technician.deleteMany({ where: { organizationId: ORG_ID } });
  914. await prisma.workBay.deleteMany({ where: { organizationId: ORG_ID } });
  915. await prisma.smsMessage.deleteMany({ where: { organizationId: ORG_ID } });
  916. await prisma.quote.deleteMany({ where: { organizationId: ORG_ID } });
  917. await prisma.statusReport.deleteMany({ where: { organizationId: ORG_ID } });
  918. // By organization, not only through the vehicle: a parts-only counter sale
  919. // has no vehicle, and scoped by vehicle it survived every reset, picking up
  920. // a fresh invoice number and payment each time. The vehicle clause stays for
  921. // any older row written without organizationId.
  922. await prisma.serviceRecord.deleteMany({
  923. where: { OR: [{ organizationId: ORG_ID }, { vehicle: { organizationId: ORG_ID } }] },
  924. });
  925. // Issued invoices froze a design, logo and signature here, held by a
  926. // RESTRICT key on service_records, which is why the work orders went first.
  927. // Customer.invoiceDesignId, ServiceRecord.designId and the inspection
  928. // snapshot links are SetNull.
  929. await prisma.documentDesign.deleteMany({ where: { organizationId: ORG_ID } });
  930. await prisma.documentDesignSnapshot.deleteMany({ where: { organizationId: ORG_ID } });
  931. await prisma.documentAssetSnapshot.deleteMany({ where: { organizationId: ORG_ID } });
  932. await prisma.fuelLog.deleteMany({ where: { vehicle: { organizationId: ORG_ID } } });
  933. // A reminder can hang off a vehicle, a customer or nothing at all, so the
  934. // vehicle cascade alone would leave workshop-level ones behind to pile up
  935. // on every reset. organizationId covers all three; the vehicle clause stays
  936. // for any older row that predates it.
  937. await prisma.reminder.deleteMany({
  938. where: { OR: [{ organizationId: ORG_ID }, { vehicle: { organizationId: ORG_ID } }] },
  939. });
  940. await prisma.note.deleteMany({ where: { vehicle: { organizationId: ORG_ID } } });
  941. await prisma.vehicle.deleteMany({ where: { organizationId: ORG_ID } });
  942. await prisma.customer.deleteMany({ where: { organizationId: ORG_ID } });
  943. await prisma.stockMovement.deleteMany({ where: { organizationId: ORG_ID } });
  944. await prisma.inventoryPart.deleteMany({ where: { organizationId: ORG_ID } });
  945. // Tire sets survive the vehicle and customer wipe above, because both of
  946. // those relations are SetNull rather than Cascade. Warehouses and shelves
  947. // only cascade from the organization, which the reset never touches. Left
  948. // alone, every reseed would stack another storeroom on top of the last.
  949. // Measurements, movements, treatments and attachments cascade from the set.
  950. await prisma.tireSet.deleteMany({ where: { organizationId: ORG_ID } });
  951. await prisma.tireLocation.deleteMany({ where: { organizationId: ORG_ID } });
  952. await prisma.tireWarehouse.deleteMany({ where: { organizationId: ORG_ID } });
  953. // Visitor-created accounts, now that nothing restricts them. Their
  954. // sessions, credentials, devices and passkeys cascade.
  955. if (visitorUserIds.length > 0) {
  956. const removedUsers = await prisma.user.deleteMany({ where: { id: { in: visitorUserIds } } });
  957. console.log(` Removed ${removedUsers.count} visitor-created account(s).`);
  958. }
  959. // The shared demo login. Every visitor signs in as this user, so its
  960. // sessions and remembered devices (IP addresses included) were shown to the
  961. // next visitor on the account page; a reset now signs everyone out. Two
  962. // factor and passkeys go too, so nobody can lock the account until the next
  963. // reset, and the dashboard returns to its default layout.
  964. await prisma.session.deleteMany({ where: { userId: USER_ID } });
  965. await prisma.userDevice.deleteMany({ where: { userId: USER_ID } });
  966. await prisma.twoFactor.deleteMany({ where: { userId: USER_ID } });
  967. await prisma.passkey.deleteMany({ where: { userId: USER_ID } });
  968. await prisma.user.update({
  969. where: { id: USER_ID },
  970. data: { dashboardLayout: Prisma.DbNull, twoFactorEnabled: false, image: null, phone: null },
  971. });
  972. cleanupUploads();
  973. console.log(" Done.\n");
  974. }
  975. async function seed() {
  976. console.log("\nStarting seed...\n");
  977. await provisionDemoAccount();
  978. await cleanup();
  979. // Every row the app scopes by organisation needs the column set, not just a
  980. // parent that has it: work orders and reminders were created without it and
  981. // the app, which reads `where: { organizationId }`, answered "Service record
  982. // not found" for all 101 seeded jobs and listed none of the 28 reminders.
  983. // Populate vehicle images: prefer bundled assets → fall back to cached copy
  984. // in data volume → fall back to live download.
  985. console.log("Populating vehicle images...");
  986. if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true });
  987. for (const [file, url] of Object.entries(vehicleImages)) {
  988. const dest = path.join(UPLOAD_DIR, file);
  989. const bundled = path.join(BUNDLED_IMAGE_DIR, file);
  990. if (fs.existsSync(bundled)) {
  991. fs.copyFileSync(bundled, dest);
  992. console.log(` [bundled] ${file}`);
  993. continue;
  994. }
  995. if (fs.existsSync(dest)) { console.log(` [cached] ${file}`); continue; }
  996. try { await downloadFile(url, dest); console.log(` [downloaded] ${file}`); }
  997. catch { console.warn(` [fail] ${file}`); }
  998. }
  999. // -- Workshop settings --
  1000. // cleanup() wipes every AppSetting, so without this the demo would run with
  1001. // no address, no currency and no invoice numbering - the settings pages and
  1002. // every invoice PDF would render blank.
  1003. console.log("\nApplying workshop settings...");
  1004. const settings: Record<string, string> = {
  1005. "workshop.address": "1450 Foundry Road, Denver, CO 80216",
  1006. "workshop.phone": "+1 (555) 555-0100",
  1007. "workshop.email": "service@egelandauto.example.com",
  1008. "workshop.serviceType": "automotive",
  1009. "workshop.unitSystem": "imperial",
  1010. "workshop.defaultLaborRate": "110",
  1011. "workshop.defaultTechnician": "Jake Wilson",
  1012. "workshop.taxEnabled": "true",
  1013. "workshop.taxInclusive": "false",
  1014. "workshop.taxLabel": "Sales Tax",
  1015. "workshop.defaultTaxRate": "8",
  1016. "workshop.currencyCode": "USD",
  1017. "workshop.currencySymbol": "$",
  1018. "workshop.currencyFormat": "symbol",
  1019. "workshop.invoicePrefix": "{year}-",
  1020. "workshop.invoiceStartNumber": "1001",
  1021. "workshop.quotePrefix": "Q-{year}-",
  1022. "workshop.quoteValidDays": "30",
  1023. "workshop.dateFormat": "MM/dd/yyyy",
  1024. "workshop.timeFormat": "12h",
  1025. "invoice.dueDays": "14",
  1026. "invoice.paymentTerms": "Net 14 days from invoice date.",
  1027. "invoice.footerNote": "Thank you for your business. All work carries a 12-month parts and labor warranty.",
  1028. "invoice.orgNumber": "US-84-2210997",
  1029. "invoice.bankAccount": "Front Range Bank · 1120 4455 8890",
  1030. "invoice.showOrgNumber": "true",
  1031. "invoice.showBankAccount": "true",
  1032. "email.fromName": "Egeland Auto",
  1033. // Settings -> Warranty (SETTING_KEYS.WARRANTY_* in settingsSchema.ts):
  1034. // every new quote and work order starts with this statement. The seeded
  1035. // invoices below carry the same terms, so old and new documents agree.
  1036. "warranty.defaultStatus": "included",
  1037. "warranty.defaultMonths": String(WARRANTY.months),
  1038. "warranty.defaultMileage": String(WARRANTY.mileage),
  1039. "warranty.defaultTerms": WARRANTY.terms,
  1040. "warranty.notIncludedText": "No workshop warranty is given on customer-supplied parts. Your statutory rights are not affected.",
  1041. // New jobs are named from the vehicle rather than a number, which is how
  1042. // the seeded ones read ("Honda Civic - AC Compressor").
  1043. "workshop.workOrderTitleTemplate": "{make} {model} - {license_plate}",
  1044. // The Regulatory certificate, as the designer saves it when the preset is
  1045. // applied. Without a designer layout a completed inspection that froze no
  1046. // design prints the old built-in sheet (certificateDesignSource()), which
  1047. // is every seeded one. The JSON is buildLayoutFromPreset() of the
  1048. // "certificate-regulator" preset in src/features/settings/Schema/
  1049. // layoutPresets.ts, stamped with DESIGNER_LAYOUT_VERSION; regenerate it
  1050. // from there if the preset changes. The look is that preset's template.
  1051. "certificate.layoutConfig": CERTIFICATE_REGULATOR_LAYOUT,
  1052. "certificate.activeDesign": "preset:certificate-regulator",
  1053. "certificate.primaryColor": "#111827",
  1054. "certificate.headerStyle": "compact",
  1055. "certificate.fontFamily": "Times-Roman",
  1056. "certificate.textColor": "#111827",
  1057. // The built-in checklists are seeded in English. Without this marker the
  1058. // first visitor's browser language would decide which language the whole
  1059. // demo's checklists get rewritten into (relocalizeBuiltinTemplates).
  1060. "inspections.presetsLocale": "en",
  1061. "workboard.weekStartDay": "1",
  1062. "workboard.workDayStart": "07:00",
  1063. "workboard.workDayEnd": "17:00",
  1064. "inventory.defaultUnit": "pcs",
  1065. "inventory.lowStockAlerts.enabled": "true",
  1066. "inventory.lowStockAlerts.inApp": "true",
  1067. "parts.defaultMarkupPercent": "35",
  1068. "portal.enabled": "true",
  1069. "portal.description": "Book a service, follow your repair and read your invoices online.",
  1070. "portal.hours": "Mon-Fri 07:00-17:00 · Sat 09:00-13:00 · Closed Sunday",
  1071. // Placeholder provider config so the messaging pages render as a
  1072. // configured workshop rather than an empty setup prompt. The credentials
  1073. // are deliberately not real, and nothing can send from the demo anyway:
  1074. // assertOutboundAllowed() in lib/email, lib/sms, lib/whatsapp and
  1075. // lib/telegram refuses every outbound call while DEMO_MODE=true.
  1076. "sms.provider": "twilio",
  1077. "sms.phoneNumber": "+15555550100",
  1078. "sms.twilio.accountSid": "ACdemo0000000000000000000000000000",
  1079. "sms.twilio.authToken": "demo-token-not-a-real-credential",
  1080. // Without the enabled flag the bot token alone gets the channel into the
  1081. // inbox, but the Telegram tab on a customer never appears, so the seeded
  1082. // conversations were only half visible.
  1083. "telegram.enabled": "true",
  1084. "telegram.botToken": "0000000000:DEMO-not-a-real-bot-token",
  1085. "telegram.botUsername": "EgelandAutoDemoBot",
  1086. // WhatsApp reaches a workshop through whichever provider will sell it a
  1087. // number, so credentials are namespaced per adapter rather than by field.
  1088. // Meta is the one most visitors will recognise.
  1089. "whatsapp.enabled": "true",
  1090. "whatsapp.provider": "meta",
  1091. "whatsapp.from": "+15555550100",
  1092. "whatsapp.cred.meta.phoneNumberId": "000000000000000",
  1093. "whatsapp.cred.meta.accessToken": "DEMO-not-a-real-access-token",
  1094. "whatsapp.cred.meta.verifyToken": "demo-verify-token",
  1095. // Two templates, because WhatsApp fixes a template's media type at
  1096. // approval and one cannot carry both a line of text and a photo.
  1097. "whatsapp.tpl.meta.text.name": "vehicle_ready",
  1098. "whatsapp.tpl.meta.text.language": "en",
  1099. "whatsapp.tpl.meta.text.variables": "customer,vehicle,message",
  1100. "whatsapp.tpl.meta.media.name": "vehicle_photo_update",
  1101. "whatsapp.tpl.meta.media.language": "en",
  1102. // Meta carries the photo as a header parameter rather than a variable, so
  1103. // the media template's blanks are the same words as the text one.
  1104. "whatsapp.tpl.meta.media.variables": "customer,vehicle,message",
  1105. // Proof the webhook plumbing works, which is otherwise invisible from
  1106. // inside the app and leaves the settings page looking half finished.
  1107. "whatsapp.webhookSeenAt": hoursAgo(2).toISOString(),
  1108. // The three channels above are also created as catalog connections
  1109. // below, which is what adopting them on first use would leave behind.
  1110. // These markers say that has happened, so a disconnect in the demo
  1111. // sticks until the next reset instead of being undone by the old rows.
  1112. "integrations.sms.adoptedAt": NOW.toISOString(),
  1113. "integrations.telegram.adoptedAt": NOW.toISOString(),
  1114. "integrations.whatsapp.adoptedAt": NOW.toISOString(),
  1115. // Tire hotel is opt-in per workshop, so the sidebar entry and the
  1116. // routes stay hidden until this is set.
  1117. "tireHotel.enabled": "true",
  1118. "tireHotel.summerReplaceMm": String(DEMO_TREAD_LIMITS.summerReplace),
  1119. "tireHotel.winterReplaceMm": String(DEMO_TREAD_LIMITS.winterReplace),
  1120. "tireHotel.defaultCapacity": "8",
  1121. "tireHotel.capacityWarnPercent": "90",
  1122. "tireHotel.defaultSeasonalPrice": "45",
  1123. "tireHotel.treatmentPrices": JSON.stringify({
  1124. wash_tires: 15,
  1125. wash_rims: 25,
  1126. balance: 40,
  1127. tpms_service: 30,
  1128. new_valves: 20,
  1129. repair: 35,
  1130. }),
  1131. };
  1132. await Promise.all(
  1133. Object.entries(settings).map(([key, value]) =>
  1134. prisma.appSetting.upsert({
  1135. where: { organizationId_key: { organizationId: ORG_ID, key } },
  1136. create: { organizationId: ORG_ID, key, value, userId: USER_ID },
  1137. update: { value },
  1138. }),
  1139. ),
  1140. );
  1141. console.log(` Applied ${Object.keys(settings).length} settings`);
  1142. // -- Integration connections --
  1143. // The messaging vendors configured above, as the catalog sees them: sealed
  1144. // credentials, the settings adoption would split out of the old rows, and
  1145. // the sending identity as the connected account. Cosmetic only: the
  1146. // credentials are not real, and DEMO_MODE refuses every outbound call and
  1147. // every connector request before it reaches the network.
  1148. console.log("\nCreating integration connections...");
  1149. const webhookSecretHash = (secret: string) => createHash("sha256").update(secret).digest("hex");
  1150. const smsWebhookSecret = randomBytes(24).toString("hex");
  1151. const telegramWebhookSecret = randomBytes(24).toString("hex");
  1152. const connections: Array<{
  1153. connectorId: string;
  1154. credentials: Record<string, string>;
  1155. settings: Record<string, unknown>;
  1156. externalAccountName: string;
  1157. }> = [
  1158. {
  1159. connectorId: "twilio-sms",
  1160. credentials: {
  1161. accountSid: settings["sms.twilio.accountSid"],
  1162. authToken: settings["sms.twilio.authToken"],
  1163. webhookSecret: smsWebhookSecret,
  1164. },
  1165. settings: {
  1166. phoneNumber: settings["sms.phoneNumber"],
  1167. webhookSecretHash: webhookSecretHash(smsWebhookSecret),
  1168. },
  1169. externalAccountName: settings["sms.phoneNumber"],
  1170. },
  1171. {
  1172. connectorId: "telegram",
  1173. credentials: {
  1174. botToken: settings["telegram.botToken"],
  1175. webhookSecret: telegramWebhookSecret,
  1176. },
  1177. settings: {
  1178. enabled: true,
  1179. botUsername: settings["telegram.botUsername"],
  1180. webhookSecretHash: webhookSecretHash(telegramWebhookSecret),
  1181. },
  1182. externalAccountName: `@${settings["telegram.botUsername"]}`,
  1183. },
  1184. {
  1185. connectorId: "whatsapp-meta",
  1186. credentials: {
  1187. phoneNumberId: settings["whatsapp.cred.meta.phoneNumberId"],
  1188. accessToken: settings["whatsapp.cred.meta.accessToken"],
  1189. verifyToken: settings["whatsapp.cred.meta.verifyToken"],
  1190. },
  1191. settings: {
  1192. enabled: true,
  1193. phoneNumber: settings["whatsapp.from"],
  1194. templateName: settings["whatsapp.tpl.meta.text.name"],
  1195. templateLanguage: settings["whatsapp.tpl.meta.text.language"],
  1196. templateVariables: settings["whatsapp.tpl.meta.text.variables"],
  1197. mediaTemplateName: settings["whatsapp.tpl.meta.media.name"],
  1198. mediaTemplateLanguage: settings["whatsapp.tpl.meta.media.language"],
  1199. mediaTemplateVariables: settings["whatsapp.tpl.meta.media.variables"],
  1200. },
  1201. externalAccountName: settings["whatsapp.from"],
  1202. },
  1203. ];
  1204. await Promise.all(
  1205. connections.map((c) =>
  1206. prisma.integrationConnection.create({
  1207. data: {
  1208. organizationId: ORG_ID,
  1209. connectorId: c.connectorId,
  1210. status: "active",
  1211. label: "Adopted from settings",
  1212. credentials: sealCredentials(c.credentials),
  1213. settings: c.settings as object,
  1214. externalAccountName: c.externalAccountName,
  1215. createdById: USER_ID,
  1216. lastHealthAt: hoursAgo(1),
  1217. createdAt: hoursAgo(24 * 90),
  1218. },
  1219. }),
  1220. ),
  1221. );
  1222. console.log(` Created ${connections.length} integration connections`);
  1223. // -- Customers (20) --
  1224. console.log("\nCreating customers...");
  1225. const customers = await Promise.all([
  1226. /* 0 */ prisma.customer.create({ data: { customerNumber: "1001", name: "Summit Construction Inc.", email: "fleet@summitconstruction.example.com", phone: "+1 (555) 555-0101", address: "742 Industrial Pkwy, Denver, CO 80216", company: "Summit Construction Inc.", notes: "Major fleet customer. 15 vehicles + heavy machinery. Net-30 terms.", userId: USER_ID, organizationId: ORG_ID } }),
  1227. /* 1 */ prisma.customer.create({ data: { customerNumber: "1002", name: "James Mitchell", email: "james.mitchell@example.com", phone: "+1 (555) 555-0102", address: "1824 Maple Ave, Austin, TX 78701", notes: "Personal vehicles. Prefers synthetic oil.", userId: USER_ID, organizationId: ORG_ID } }),
  1228. /* 2 */ prisma.customer.create({ data: { customerNumber: "1003", name: "Pacific Freight Lines", email: "service@pacificfreight.example.com", phone: "+1 (555) 555-0103", address: "9100 Logistics Blvd, Portland, OR 97201", company: "Pacific Freight Lines LLC", notes: "Long-haul trucking company. 8 semi-trucks. Priority service.", userId: USER_ID, organizationId: ORG_ID } }),
  1229. /* 3 */ prisma.customer.create({ data: { customerNumber: "1004", name: "Sarah Coleman", email: "sarah.coleman@example.com", phone: "+1 (555) 555-0104", address: "567 Oak Street, Nashville, TN 37203", notes: "BMW and Porsche owner. Always requests OEM parts.", userId: USER_ID, organizationId: ORG_ID } }),
  1230. /* 4 */ prisma.customer.create({ data: { customerNumber: "1005", name: "Metro City Services", email: "fleet@metrocityservices.example.com", phone: "+1 (555) 555-0105", address: "100 City Hall Plaza, Boston, MA 02108", company: "Metro City Services", notes: "Municipal fleet contract. Quarterly invoicing. Purchase order required.", userId: USER_ID, organizationId: ORG_ID } }),
  1231. /* 5 */ prisma.customer.create({ data: { customerNumber: "1006", name: "Ryan Parker", email: "ryan.parker@example.com", phone: "+1 (555) 555-0106", address: "2250 Cedar Lane, Seattle, WA 98101", notes: "Tesla and Subaru owner. Tech-savvy.", userId: USER_ID, organizationId: ORG_ID } }),
  1232. /* 6 */ prisma.customer.create({ data: { customerNumber: "1007", name: "Ironclad Equipment Co.", email: "parts@ironcladequip.example.com", phone: "+1 (555) 555-0107", address: "4500 Heavy Machinery Rd, Houston, TX 77001", company: "Ironclad Equipment Co.", notes: "Heavy equipment dealer. Sends us overflow work. Good referral source.", userId: USER_ID, organizationId: ORG_ID } }),
  1233. /* 7 */ prisma.customer.create({ data: { customerNumber: "1008", name: "David Chen", email: "david.chen@example.com", phone: "+1 (555) 555-0108", address: "890 Pine Street, San Francisco, CA 94102", notes: "Audi and VW owner. Referred by James Mitchell.", userId: USER_ID, organizationId: ORG_ID } }),
  1234. /* 8 */ prisma.customer.create({ data: { customerNumber: "1009", name: "Ridgeline Excavation LLC", email: "ops@ridgelineexcavation.example.com", phone: "+1 (555) 555-0109", address: "1200 Quarry Rd, Salt Lake City, UT 84101", company: "Ridgeline Excavation LLC", notes: "5 excavators, 2 wheel loaders, 3 dump trucks. Monthly maintenance contract.", userId: USER_ID, organizationId: ORG_ID } }),
  1235. /* 9 */ prisma.customer.create({ data: { customerNumber: "1010", name: "Amanda Foster", email: "amanda.foster@example.com", phone: "+1 (555) 555-0110", address: "334 Birch Drive, Charlotte, NC 28202", notes: "Honda Civic owner. Budget-conscious, prefers aftermarket.", userId: USER_ID, organizationId: ORG_ID } }),
  1236. /* 10 */ prisma.customer.create({ data: { customerNumber: "1011", name: "Prairie Farms Co-op", email: "maintenance@prairiefarms.example.com", phone: "+1 (555) 555-0111", address: "Route 66 Farm Road, Omaha, NE 68101", company: "Prairie Farms Co-op", notes: "Agricultural equipment. Tractors, harvesters. Seasonal peaks spring/fall.", userId: USER_ID, organizationId: ORG_ID } }),
  1237. /* 11 */ prisma.customer.create({ data: { customerNumber: "1012", name: "Mike Thompson", email: "mike.thompson@example.com", phone: "+1 (555) 555-0112", address: "445 Elm Court, Minneapolis, MN 55401", notes: "Ford F-150 and Jeep owner. Off-road enthusiast.", userId: USER_ID, organizationId: ORG_ID } }),
  1238. /* 12 */ prisma.customer.create({ data: { customerNumber: "1013", name: "Apex Contractors Group", email: "service@apexcontractors.example.com", phone: "+1 (555) 555-0113", address: "2800 Builder's Row, Atlanta, GA 30301", company: "Apex Contractors Group", notes: "Small contractor. 2 excavators, 1 dump truck, 2 vans.", userId: USER_ID, organizationId: ORG_ID } }),
  1239. /* 13 */ prisma.customer.create({ data: { customerNumber: "1014", name: "Jessica Rivera", email: "jessica.r@example.com", phone: "+1 (555) 555-0114", address: "1100 Sunset Blvd, Miami, FL 33101", notes: "Tesla Model Y owner. Referred by Ryan Parker.", userId: USER_ID, organizationId: ORG_ID } }),
  1240. /* 14 */ prisma.customer.create({ data: { customerNumber: "1015", name: "Titan Building Corp.", email: "projects@titanbuildingcorp.example.com", phone: "+1 (555) 555-0115", address: "5000 Commerce Dr, Dallas, TX 75201", company: "Titan Building Corp.", notes: "Large construction firm. 20+ pieces of equipment. Net-45 terms.", userId: USER_ID, organizationId: ORG_ID } }),
  1241. /* 15 */ prisma.customer.create({ data: { customerNumber: "1016", name: "Kevin O'Brien", email: "kevin.obrien@example.com", phone: "+1 (555) 555-0116", address: "678 Market Street, Philadelphia, PA 19101", notes: "Mercedes Sprinter van for his catering business.", userId: USER_ID, organizationId: ORG_ID } }),
  1242. /* 16 */ prisma.customer.create({ data: { customerNumber: "1017", name: "Greenfield Waste Management", email: "dispatch@greenfieldwaste.example.com", phone: "+1 (555) 555-0117", address: "7700 Recycling Way, Phoenix, AZ 85001", company: "Greenfield Waste Management", notes: "Waste management fleet. 12 trucks. Quarterly PM schedule.", userId: USER_ID, organizationId: ORG_ID } }),
  1243. /* 17 */ prisma.customer.create({ data: { customerNumber: "1018", name: "Robert Hughes", email: "robert.hughes@example.com", phone: "+1 (555) 555-0118", address: "2100 Lakeview Ave, Detroit, MI 48201", notes: "Ram truck and VW Golf owner. Weekend car guy.", userId: USER_ID, organizationId: ORG_ID } }),
  1244. /* 17 */ prisma.customer.create({ data: { customerNumber: "1019", name: "Heartland Grain Co.", email: "ops@heartlandgrain.example.com", phone: "+1 (555) 555-0119", address: "4400 Harvest Rd, Des Moines, IA 50301", company: "Heartland Grain Co.", notes: "Large-scale farming. 6 tractors, 2 combines. Seasonal rush spring and fall.", userId: USER_ID, organizationId: ORG_ID } }),
  1245. /* 19 */ prisma.customer.create({ data: { customerNumber: "1020", name: "Lisa Martinez", email: "lisa.martinez@example.com", phone: "+1 (555) 555-0120", address: "425 Ranch Road, San Antonio, TX 78201", notes: "Chevy Silverado and Toyota Tacoma. Ranch use.", userId: USER_ID, organizationId: ORG_ID } }),
  1246. // The one address on a domain we own, for checking a send end to end
  1247. // without it ever leaving Torqvoice. Everyone else sits on example.com.
  1248. /* 20 */ prisma.customer.create({ data: { customerNumber: "1021", name: "Jennifer Hayes", email: "jennifer@torqvoice.com", phone: "+1 (555) 555-0121", address: "88 Copper Lane, Boulder, CO 80301", notes: "Test account for message delivery checks.", userId: USER_ID, organizationId: ORG_ID } }),
  1249. ]);
  1250. console.log(` Created ${customers.length} customers`);
  1251. // -- Vehicles (40) --
  1252. console.log("\nCreating vehicles...");
  1253. const vehicles = await Promise.all([
  1254. // === Cars (0-17) ===
  1255. /* 0 */ prisma.vehicle.create({ data: { make: "Toyota", model: "Camry XSE", year: 2023, vin: "4T1K61AK5PU083241", licensePlate: "TX-8821", color: "Midnight Black", mileage: 18500, fuelType: "gasoline", transmission: "automatic", engineSize: "2.5L", purchaseDate: new Date("2023-03-15"), purchasePrice: 35500, imageUrl: img("vehicles","toyota-camry.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[1].id } }),
  1256. /* 1 */ prisma.vehicle.create({ data: { make: "Ford", model: "F-150 Lariat", year: 2022, vin: "1FTFW1E88NFC12345", licensePlate: "CO-4455", color: "Oxford White", mileage: 42300, fuelType: "gasoline", transmission: "automatic", engineSize: "3.5L EcoBoost", purchaseDate: new Date("2022-06-20"), purchasePrice: 58000, imageUrl: img("vehicles","ford-f150-white.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[0].id } }),
  1257. /* 2 */ prisma.vehicle.create({ data: { make: "Ford", model: "F-150 XLT", year: 2021, vin: "1FTEW1EP5MKD56789", licensePlate: "MN-7766", color: "Carbonized Gray", mileage: 55000, fuelType: "gasoline", transmission: "automatic", engineSize: "2.7L EcoBoost", purchaseDate: new Date("2021-04-10"), purchasePrice: 45000, imageUrl: img("vehicles","ford-f150-grey.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[11].id } }),
  1258. /* 3 */ prisma.vehicle.create({ data: { make: "Ford", model: "F-150 Platinum", year: 2024, vin: "1FTFW1E55RFA98765", licensePlate: "TX-1199", color: "Agate Black", mileage: 12000, fuelType: "gasoline", transmission: "automatic", engineSize: "3.5L EcoBoost V6", purchaseDate: new Date("2024-02-14"), purchasePrice: 72000, imageUrl: img("vehicles","ford-f150-black.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[4].id } }),
  1259. /* 4 */ prisma.vehicle.create({ data: { make: "BMW", model: "330i xDrive", year: 2021, vin: "WBA5R1C55M7B98765", licensePlate: "TN-2233", color: "Alpine White", mileage: 55200, fuelType: "gasoline", transmission: "automatic", engineSize: "2.0L Turbo", purchaseDate: new Date("2021-01-10"), purchasePrice: 46000, imageUrl: img("vehicles","bmw-3series.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[3].id } }),
  1260. /* 5 */ prisma.vehicle.create({ data: { make: "Honda", model: "Civic Sport", year: 2020, vin: "2HGFC2F63LH567890", licensePlate: "NC-6677", color: "Rallye Red", mileage: 67800, fuelType: "gasoline", transmission: "manual", engineSize: "2.0L", purchaseDate: new Date("2020-08-05"), purchasePrice: 26000, imageUrl: img("vehicles","honda-civic.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[9].id } }),
  1261. /* 6 */ prisma.vehicle.create({ data: { make: "Tesla", model: "Model 3 Long Range", year: 2024, vin: "5YJ3E1EA3RF123456", licensePlate: "WA-9901", color: "Pearl White", mileage: 8200, fuelType: "electric", transmission: "automatic", engineSize: "Dual Motor", purchaseDate: new Date("2024-01-20"), purchasePrice: 47000, imageUrl: img("vehicles","tesla-model3.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[5].id } }),
  1262. /* 7 */ prisma.vehicle.create({ data: { make: "Tesla", model: "Model Y Long Range", year: 2025, vin: "7SAYGDEE3SF123456", licensePlate: "FL-7720", color: "Quicksilver", mileage: 3200, fuelType: "electric", transmission: "automatic", engineSize: "Dual Motor AWD", purchaseDate: new Date("2025-06-10"), purchasePrice: 52000, imageUrl: img("vehicles","tesla-modely.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[13].id } }),
  1263. /* 8 */ prisma.vehicle.create({ data: { make: "Chevrolet", model: "Silverado 2500HD", year: 2019, vin: "1GC4YREY5KF234567", licensePlate: "TX-3344", color: "Shadow Gray", mileage: 98400, fuelType: "diesel", transmission: "automatic", engineSize: "6.6L Duramax", purchaseDate: new Date("2019-04-12"), purchasePrice: 52000, imageUrl: img("vehicles","chevy-silverado.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[19].id } }),
  1264. /* 9 */ prisma.vehicle.create({ data: { make: "Mercedes-Benz", model: "Sprinter 316 CDI", year: 2022, vin: "WDB9066331S789012", licensePlate: "PA-1122", color: "Arctic White", mileage: 35600, fuelType: "diesel", transmission: "automatic", engineSize: "2.1L CDI", purchaseDate: new Date("2022-09-01"), purchasePrice: 55000, imageUrl: img("vehicles","mercedes-sprinter.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[15].id } }),
  1265. /* 10 */ prisma.vehicle.create({ data: { make: "Audi", model: "A4 Quattro", year: 2023, vin: "WAUDNAF44PN345678", licensePlate: "CA-5566", color: "Navarra Blue", mileage: 22100, fuelType: "gasoline", transmission: "automatic", engineSize: "2.0L TFSI", purchaseDate: new Date("2023-07-14"), purchasePrice: 45000, imageUrl: img("vehicles","audi-a4.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[7].id } }),
  1266. /* 11 */ prisma.vehicle.create({ data: { make: "Jeep", model: "Wrangler Rubicon", year: 2022, vin: "1C4HJXFN3NW234567", licensePlate: "CO-8800", color: "Black", mileage: 28500, fuelType: "gasoline", transmission: "manual", engineSize: "3.6L Pentastar V6", purchaseDate: new Date("2022-05-18"), purchasePrice: 52000, imageUrl: img("vehicles","jeep-wrangler.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[11].id } }),
  1267. /* 12 */ prisma.vehicle.create({ data: { make: "Jeep", model: "Wrangler Sport S", year: 2023, vin: "1C4GJXAG4PW345678", licensePlate: "GA-5511", color: "Punk'n Orange", mileage: 15200, fuelType: "gasoline", transmission: "automatic", engineSize: "2.0L Turbo", purchaseDate: new Date("2023-08-22"), purchasePrice: 42000, imageUrl: img("vehicles","jeep-wrangler-orange.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[12].id } }),
  1268. /* 13 */ prisma.vehicle.create({ data: { make: "Porsche", model: "911 Carrera S", year: 2022, vin: "WP0AB2A94NS234567", licensePlate: "TN-9911", color: "White", mileage: 12800, fuelType: "gasoline", transmission: "automatic", engineSize: "3.0L Twin-Turbo Flat-6", purchaseDate: new Date("2022-11-05"), purchasePrice: 128000, imageUrl: img("vehicles","porsche-911.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[3].id } }),
  1269. /* 14 */ prisma.vehicle.create({ data: { make: "Volkswagen", model: "Golf GTI", year: 2021, vin: "3VW6T7AU5MM123456", licensePlate: "CA-4422", color: "Tornado Red", mileage: 38500, fuelType: "gasoline", transmission: "manual", engineSize: "2.0L TSI", purchaseDate: new Date("2021-06-15"), purchasePrice: 32000, imageUrl: img("vehicles","vw-golf-gti-red.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[7].id } }),
  1270. /* 15 */ prisma.vehicle.create({ data: { make: "Ram", model: "1500 Laramie", year: 2023, vin: "1C6SRFJT4PN456789", licensePlate: "MI-3322", color: "Billet Silver", mileage: 19800, fuelType: "gasoline", transmission: "automatic", engineSize: "5.7L HEMI V8", purchaseDate: new Date("2023-03-28"), purchasePrice: 62000, imageUrl: img("vehicles","ram-1500.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[17].id } }),
  1271. /* 16 */ prisma.vehicle.create({ data: { make: "Toyota", model: "Tacoma TRD Off-Road", year: 2022, vin: "3TMCZ5AN5NM567890", licensePlate: "TX-6644", color: "Super White", mileage: 31200, fuelType: "gasoline", transmission: "automatic", engineSize: "3.5L V6", purchaseDate: new Date("2022-10-01"), purchasePrice: 42000, imageUrl: img("vehicles","toyota-tacoma.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[19].id } }),
  1272. // === Trucks (17-21) ===
  1273. /* 17 */ prisma.vehicle.create({ data: { make: "Kenworth", model: "T680 Next Gen", year: 2022, vin: "1XKYD49X4NJ456789", licensePlate: "OR-8899", color: "Red", mileage: 185000, fuelType: "diesel", transmission: "automatic", engineSize: "12.9L PACCAR MX-13", purchaseDate: new Date("2022-02-15"), purchasePrice: 175000, imageUrl: img("vehicles","kenworth-t680.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[2].id } }),
  1274. /* 18 */ prisma.vehicle.create({ data: { make: "Volvo", model: "FH 640", year: 2023, vin: "YV2RT40A5PB567890", licensePlate: "IL-5567", color: "Blue", mileage: 92000, fuelType: "diesel", transmission: "automatic", engineSize: "12.8L D13TC", purchaseDate: new Date("2023-01-10"), purchasePrice: 210000, imageUrl: img("vehicles","volvo-fh640.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[2].id } }),
  1275. /* 19 */ prisma.vehicle.create({ data: { make: "Mack", model: "Granite GR64F", year: 2020, vin: "1M2AX04C5LM012345", licensePlate: "AZ-9901", color: "Yellow", mileage: 156000, fuelType: "diesel", transmission: "automatic", engineSize: "13.0L MP7", purchaseDate: new Date("2020-11-08"), purchasePrice: 140000, imageUrl: img("vehicles","mack-granite.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[0].id } }),
  1276. /* 20 */ prisma.vehicle.create({ data: { make: "Peterbilt", model: "567 Mixer", year: 2021, vin: "1NPCD49X1MD678901", licensePlate: "TX-7788", color: "White/Blue", mileage: 68000, fuelType: "diesel", transmission: "automatic", engineSize: "10.8L PACCAR MX-11", purchaseDate: new Date("2021-07-20"), purchasePrice: 165000, imageUrl: img("vehicles","concrete-mixer.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[14].id } }),
  1277. /* 21 */ prisma.vehicle.create({ data: { make: "Kenworth", model: "T880 Dump Truck", year: 2022, vin: "1NKZX4TX8NJ789012", licensePlate: "CO-5544", color: "Yellow", mileage: 82000, fuelType: "diesel", transmission: "automatic", engineSize: "12.9L PACCAR MX-13", purchaseDate: new Date("2022-04-15"), purchasePrice: 185000, imageUrl: img("vehicles","dump-truck.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[8].id } }),
  1278. // === Tractors (22-28) ===
  1279. /* 22 */ prisma.vehicle.create({ data: { make: "John Deere", model: "6R 250", year: 2022, vin: "1L06250RCNR012345", licensePlate: "N/A", color: "John Deere Green", mileage: 2800, fuelType: "diesel", transmission: "powershift", engineSize: "6.8L PowerTech PVS", purchaseDate: new Date("2022-03-01"), purchasePrice: 185000, imageUrl: img("vehicles","john-deere-6r.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[10].id } }),
  1280. /* 23 */ prisma.vehicle.create({ data: { make: "Fendt", model: "942 Vario", year: 2023, vin: "FEN942VARIO3N56789", licensePlate: "N/A", color: "Fendt Green", mileage: 1200, fuelType: "diesel", transmission: "CVT (Vario)", engineSize: "12.4L MAN D2676", purchaseDate: new Date("2023-06-15"), purchasePrice: 420000, imageUrl: img("vehicles","fendt-942.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[18].id } }),
  1281. /* 24 */ prisma.vehicle.create({ data: { make: "Massey Ferguson", model: "8S.265", year: 2021, vin: "MF8S265A1LM234567", licensePlate: "N/A", color: "Red", mileage: 3600, fuelType: "diesel", transmission: "Dyna-VT CVT", engineSize: "8.4L AGCO Power", purchaseDate: new Date("2021-09-10"), purchasePrice: 245000, imageUrl: img("vehicles","massey-ferguson.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[10].id } }),
  1282. /* 25 */ prisma.vehicle.create({ data: { make: "John Deere", model: "8R 410", year: 2024, vin: "1RW8410RPEN345678", licensePlate: "N/A", color: "Green/Yellow", mileage: 800, fuelType: "diesel", transmission: "e23 PowerShift", engineSize: "9.0L PowerTech PSS", purchaseDate: new Date("2024-01-20"), purchasePrice: 380000, imageUrl: img("vehicles","tractor-wheat.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[18].id } }),
  1283. /* 26 */ prisma.vehicle.create({ data: { make: "New Holland", model: "T7.315", year: 2022, vin: "NH7315ACLM456789", licensePlate: "N/A", color: "New Holland Blue", mileage: 2100, fuelType: "diesel", transmission: "Auto Command CVT", engineSize: "6.7L NEF", purchaseDate: new Date("2022-08-05"), purchasePrice: 210000, imageUrl: img("vehicles","tractor-field.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[10].id } }),
  1284. /* 27 */ prisma.vehicle.create({ data: { make: "John Deere", model: "S780 Combine", year: 2023, vin: "1H0S780SPN567890", licensePlate: "N/A", color: "Green/Yellow", mileage: 600, fuelType: "diesel", transmission: "hydrostatic", engineSize: "13.5L PowerTech PSS", purchaseDate: new Date("2023-05-10"), purchasePrice: 520000, imageUrl: img("vehicles","combine-harvester.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[18].id } }),
  1285. /* 28 */ prisma.vehicle.create({ data: { make: "John Deere", model: "X9 1100 Combine", year: 2024, vin: "1H0X91100RN678901", licensePlate: "N/A", color: "Green/Yellow", mileage: 350, fuelType: "diesel", transmission: "hydrostatic", engineSize: "13.6L PowerTech", purchaseDate: new Date("2024-03-15"), purchasePrice: 680000, imageUrl: img("vehicles","combine-jd.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[18].id } }),
  1286. // === Heavy Machinery (29-37) ===
  1287. /* 29 */ prisma.vehicle.create({ data: { make: "Caterpillar", model: "D6 XE Bulldozer", year: 2021, vin: "CAT0D6XE2LM012345", licensePlate: "N/A", color: "CAT Yellow", mileage: 4200, fuelType: "diesel", transmission: "automatic", engineSize: "9.3L C9.3B", purchaseDate: new Date("2021-03-25"), purchasePrice: 320000, imageUrl: img("vehicles","cat-d6-bulldozer.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[0].id } }),
  1288. /* 30 */ prisma.vehicle.create({ data: { make: "Caterpillar", model: "D8T Bulldozer", year: 2020, vin: "CAT0D8T2KM234567", licensePlate: "N/A", color: "CAT Yellow", mileage: 5800, fuelType: "diesel", transmission: "automatic", engineSize: "15.2L C15", purchaseDate: new Date("2020-06-10"), purchasePrice: 480000, imageUrl: img("vehicles","bulldozer-rocks.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[14].id } }),
  1289. /* 31 */ prisma.vehicle.create({ data: { make: "Komatsu", model: "PC210LC-11 Excavator", year: 2022, vin: "KMTPC210LCN234567", licensePlate: "N/A", color: "Komatsu Blue/White", mileage: 3100, fuelType: "diesel", transmission: "hydraulic", engineSize: "6.7L SAA6D107E-3", purchaseDate: new Date("2022-07-12"), purchasePrice: 265000, imageUrl: img("vehicles","komatsu-excavator.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[8].id } }),
  1290. /* 32 */ prisma.vehicle.create({ data: { make: "Komatsu", model: "PC490LC-11 Excavator", year: 2023, vin: "KMTPC490LCN345678", licensePlate: "N/A", color: "Komatsu Yellow", mileage: 1800, fuelType: "diesel", transmission: "hydraulic", engineSize: "15.2L SAA6D170E-7", purchaseDate: new Date("2023-01-20"), purchasePrice: 450000, imageUrl: img("vehicles","excavator-soil.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[8].id } }),
  1291. /* 33 */ prisma.vehicle.create({ data: { make: "Volvo", model: "EC220E Excavator", year: 2023, vin: "VOLEC220EN3M345678", licensePlate: "N/A", color: "Volvo Yellow", mileage: 1500, fuelType: "diesel", transmission: "hydraulic", engineSize: "5.7L D6J", purchaseDate: new Date("2023-02-20"), purchasePrice: 280000, imageUrl: img("vehicles","volvo-ec220e.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[8].id } }),
  1292. /* 34 */ prisma.vehicle.create({ data: { make: "Kubota", model: "KX040-4 Mini Excavator", year: 2023, vin: "KUB040KX4N3456789", licensePlate: "N/A", color: "Orange", mileage: 900, fuelType: "diesel", transmission: "hydraulic", engineSize: "1.8L D1803-CR-TE4", purchaseDate: new Date("2023-09-15"), purchasePrice: 62000, imageUrl: img("vehicles","mini-excavator.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[12].id } }),
  1293. /* 35 */ prisma.vehicle.create({ data: { make: "Case", model: "621G Wheel Loader", year: 2023, vin: "CASE621GN3M456789", licensePlate: "N/A", color: "Power Tan", mileage: 1800, fuelType: "diesel", transmission: "automatic", engineSize: "6.7L FPT", purchaseDate: new Date("2023-04-18"), purchasePrice: 185000, imageUrl: img("vehicles","wheel-loader.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[4].id } }),
  1294. /* 36 */ prisma.vehicle.create({ data: { make: "Liebherr", model: "LTM 1100-4.2 Crane", year: 2019, vin: "LBH1100LTM9K12345", licensePlate: "GA-2233", color: "Yellow", mileage: 12500, fuelType: "diesel", transmission: "automatic", engineSize: "10.7L D936", purchaseDate: new Date("2019-08-14"), purchasePrice: 850000, imageUrl: img("vehicles","liebherr-crane.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[14].id } }),
  1295. /* 37 */ prisma.vehicle.create({ data: { make: "Toyota", model: "8FBE18 Forklift", year: 2022, vin: "TYT8FBE18N2M56789", licensePlate: "N/A", color: "Orange/Black", mileage: 4200, fuelType: "electric", transmission: "automatic", engineSize: "48V AC Motor", purchaseDate: new Date("2022-11-01"), purchasePrice: 35000, imageUrl: img("vehicles","forklift.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[6].id } }),
  1296. // === Additional Vehicles (38-41) ===
  1297. /* 38 */ prisma.vehicle.create({ data: { make: "Chevrolet", model: "Corvette Stingray", year: 2024, vin: "1G1YB2D48R5100234", licensePlate: "TN-0038", color: "Black", mileage: 6200, fuelType: "gasoline", transmission: "automatic", engineSize: "6.2L LT2 V8", purchaseDate: new Date("2024-05-10"), purchasePrice: 68000, imageUrl: img("vehicles","corvette-c8.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[3].id } }),
  1298. /* 39 */ prisma.vehicle.create({ data: { make: "Land Rover", model: "Defender 110 S", year: 2023, vin: "SALE7EEU5P1234567", licensePlate: "CO-3911", color: "Santorini Black", mileage: 28500, fuelType: "gasoline", transmission: "automatic", engineSize: "3.0L I6 Turbo", purchaseDate: new Date("2023-06-22"), purchasePrice: 62000, imageUrl: img("vehicles","land-rover-defender.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[11].id } }),
  1299. /* 40 */ prisma.vehicle.create({ data: { make: "Dodge", model: "Charger R/T", year: 2022, vin: "2C3CDXCT5NH234567", licensePlate: "MI-4022", color: "Go Mango Orange", mileage: 34800, fuelType: "gasoline", transmission: "automatic", engineSize: "5.7L HEMI V8", purchaseDate: new Date("2022-08-15"), purchasePrice: 45000, imageUrl: img("vehicles","dodge-charger.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[17].id } }),
  1300. /* 41 */ prisma.vehicle.create({ data: { make: "Subaru", model: "WRX Premium", year: 2024, vin: "JF1VA1E68R9234567", licensePlate: "CA-4124", color: "WR Blue Pearl", mileage: 12400, fuelType: "gasoline", transmission: "manual", engineSize: "2.4L Turbo Boxer", purchaseDate: new Date("2024-03-01"), purchasePrice: 35000, imageUrl: img("vehicles","subaru-wrx.jpg"), userId: USER_ID, organizationId: ORG_ID, customerId: customers[7].id } }),
  1301. ]);
  1302. console.log(` Created ${vehicles.length} vehicles`);
  1303. // Which drawing the condition map marks each vehicle on (BODY_TYPES in
  1304. // src/features/condition-map/Lib/drawingTypes.ts). Unset falls back to a
  1305. // sedan, which is wrong for every pickup, van and SUV here. There are no
  1306. // drawings for trucks, tractors or plant, so those stay unset: no body type
  1307. // fits a combine, and the sedan fallback is at least an honest default.
  1308. // The coupes (911, Corvette) have no drawing of their own either and use
  1309. // the sedan.
  1310. const VEHICLE_BODY_TYPES: Record<number, "sedan" | "hatchback" | "estate" | "suv" | "van" | "pickup"> = {
  1311. 0: "sedan", // Toyota Camry
  1312. 1: "pickup", // Ford F-150 Lariat
  1313. 2: "pickup", // Ford F-150 XLT
  1314. 3: "pickup", // Ford F-150 Platinum
  1315. 4: "sedan", // BMW 330i
  1316. 5: "hatchback", // Honda Civic Sport (FK7 hatch)
  1317. 6: "sedan", // Tesla Model 3
  1318. 7: "suv", // Tesla Model Y
  1319. 8: "pickup", // Chevrolet Silverado 2500HD
  1320. 9: "van", // Mercedes-Benz Sprinter
  1321. 10: "sedan", // Audi A4
  1322. 11: "suv", // Jeep Wrangler Rubicon
  1323. 12: "suv", // Jeep Wrangler Sport S
  1324. 13: "sedan", // Porsche 911
  1325. 14: "hatchback", // VW Golf GTI
  1326. 15: "pickup", // Ram 1500
  1327. 16: "pickup", // Toyota Tacoma
  1328. 38: "sedan", // Chevrolet Corvette
  1329. 39: "suv", // Land Rover Defender 110
  1330. 40: "sedan", // Dodge Charger
  1331. 41: "sedan", // Subaru WRX
  1332. };
  1333. await Promise.all(
  1334. Object.entries(VEHICLE_BODY_TYPES).map(([idx, bodyType]) =>
  1335. prisma.vehicle.update({ where: { id: vehicles[Number(idx)].id }, data: { bodyType } }),
  1336. ),
  1337. );
  1338. const bodyTypeOf = (idx: number) => VEHICLE_BODY_TYPES[idx] ?? "sedan";
  1339. // -- Service Records (20) --
  1340. console.log("\nCreating service records...");
  1341. const svcData = [
  1342. { vehicleId: vehicles[0].id, title: "Oil Change & Tire Rotation", description: "Regular 10,000 mi service. Synthetic 0W-20 oil. Rotated tires front to back.", type: "maintenance", status: "completed", serviceDate: days(-114), mileage: 15000, techName: "Jake Wilson", shopName: "Egeland Auto",
  1343. partItems: [{ name: "Synthetic Oil 0W-20 (5qt)", partNumber: "TOY-0W20-5Q", quantity: 1, unitPrice: 42, total: 42 }, { name: "Oil Filter", partNumber: "TOY-OF-2023", quantity: 1, unitPrice: 12, total: 12 }],
  1344. laborItems: [{ description: "Oil change and filter replacement", hours: 0.5, rate: 95, total: 47.50 }, { description: "Tire rotation and pressure check", hours: 0.3, rate: 95, total: 28.50 }],
  1345. subtotal: 130, taxRate: 8, taxAmount: 10.40, totalAmount: 140.40 },
  1346. { vehicleId: vehicles[4].id, title: "50,000 Mile Major Service", description: "Major service interval. Replaced brake pads, rotors, spark plugs. Full inspection.", type: "maintenance", status: "completed", serviceDate: days(-140), mileage: 50000, techName: "Chris Taylor", shopName: "Egeland Auto",
  1347. partItems: [{ name: "Front Brake Pad Set", partNumber: "BMW-BP-F34", quantity: 1, unitPrice: 185, total: 185 }, { name: "Front Brake Rotors (pair)", partNumber: "BMW-BR-F34", quantity: 1, unitPrice: 320, total: 320 }, { name: "Spark Plugs (set of 4)", partNumber: "BMW-SP-N20", quantity: 1, unitPrice: 68, total: 68 }, { name: "Engine Oil 5W-30 (7qt)", partNumber: "BMW-5W30-7Q", quantity: 1, unitPrice: 89, total: 89 }],
  1348. laborItems: [{ description: "Front brake pad and rotor replacement", hours: 2.0, rate: 110, total: 220 }, { description: "Spark plug replacement", hours: 1.0, rate: 110, total: 110 }, { description: "Oil change and multi-point inspection", hours: 1.0, rate: 110, total: 110 }],
  1349. subtotal: 1102, taxRate: 8, taxAmount: 88.16, totalAmount: 1190.16 },
  1350. { vehicleId: vehicles[6].id, title: "Tire Replacement - All Four", description: "Replaced all four tires. Michelin Pilot Sport 4S. Alignment check passed.", type: "repair", status: "completed", serviceDate: days(-60), mileage: 7800, techName: "Jake Wilson", shopName: "Egeland Auto",
  1351. partItems: [{ name: "Michelin Pilot Sport 4S 235/40R19", partNumber: "MICH-PS4S-2354019", quantity: 4, unitPrice: 310, total: 1240 }],
  1352. laborItems: [{ description: "Mount and balance 4 tires", hours: 1.5, rate: 95, total: 142.50 }, { description: "Wheel alignment check", hours: 0.5, rate: 95, total: 47.50 }],
  1353. subtotal: 1430, taxRate: 8, taxAmount: 114.40, totalAmount: 1544.40 },
  1354. { vehicleId: vehicles[13].id, title: "Porsche 911 - Annual Service", description: "Annual service. Oil change, brake fluid flush, cabin filter. Full inspection.", type: "maintenance", status: "completed", serviceDate: days(-53), mileage: 12500, techName: "Chris Taylor", shopName: "Egeland Auto",
  1355. partItems: [{ name: "Porsche Approved Oil 0W-40 (9qt)", partNumber: "POR-0W40-9Q", quantity: 1, unitPrice: 165, total: 165 }, { name: "Oil Filter", partNumber: "POR-OF-992", quantity: 1, unitPrice: 28, total: 28 }, { name: "Brake Fluid DOT4 (1L)", partNumber: "POR-BF-DOT4", quantity: 2, unitPrice: 35, total: 70 }],
  1356. laborItems: [{ description: "Oil and filter change", hours: 1.0, rate: 145, total: 145 }, { description: "Brake fluid flush all 4 corners", hours: 1.5, rate: 145, total: 217.50 }, { description: "Multi-point inspection", hours: 1.0, rate: 145, total: 145 }],
  1357. subtotal: 770.50, taxRate: 8, taxAmount: 61.64, totalAmount: 832.14 },
  1358. { vehicleId: vehicles[17].id, title: "150K Mile Engine Service - Kenworth", description: "Major engine service. Replaced fuel filters, air filters, coolant flush. DPF regen.", type: "maintenance", status: "completed", serviceDate: days(-94), mileage: 180000, techName: "Marcus Reed", shopName: "Egeland Auto",
  1359. partItems: [{ name: "PACCAR MX-13 Fuel Filter Kit", partNumber: "PAC-FF-MX13", quantity: 1, unitPrice: 125, total: 125 }, { name: "Air Filter Element", partNumber: "PAC-AF-MX13", quantity: 1, unitPrice: 85, total: 85 }, { name: "Coolant (5gal)", partNumber: "PAC-COOL-5G", quantity: 2, unitPrice: 65, total: 130 }, { name: "Engine Oil 15W-40 (10gal)", partNumber: "PAC-15W40-10G", quantity: 1, unitPrice: 280, total: 280 }],
  1360. laborItems: [{ description: "Engine oil and filter change", hours: 2.0, rate: 125, total: 250 }, { description: "Fuel filter replacement", hours: 1.0, rate: 125, total: 125 }, { description: "Coolant flush and refill", hours: 1.5, rate: 125, total: 187.50 }, { description: "DPF regeneration and diagnostic", hours: 1.5, rate: 125, total: 187.50 }],
  1361. subtotal: 1370, taxRate: 8, taxAmount: 109.60, totalAmount: 1479.60 },
  1362. { vehicleId: vehicles[18].id, title: "Volvo FH 640 - DOT Inspection", description: "Full annual inspection per DOT regulations. Brake test, emissions, lights, safety.", type: "inspection", status: "in-progress", serviceDate: days(-19), mileage: 92000, techName: "Marcus Reed", shopName: "Egeland Auto",
  1363. partItems: [{ name: "Marker Light Bulbs (pack of 10)", partNumber: "VOL-MLB-10", quantity: 1, unitPrice: 35, total: 35 }, { name: "Wiper Blades (pair)", partNumber: "VOL-WB-FH", quantity: 1, unitPrice: 52, total: 52 }],
  1364. laborItems: [{ description: "Full DOT inspection and documentation", hours: 4.0, rate: 125, total: 500 }, { description: "Minor repairs and adjustments", hours: 2.0, rate: 125, total: 250 }],
  1365. subtotal: 837, taxRate: 8, taxAmount: 66.96, totalAmount: 903.96 },
  1366. { vehicleId: vehicles[22].id, title: "John Deere 6R 250 - 500hr Service", description: "Scheduled 500-hour service. Engine oil, all filters, hydraulic fluid sample, PTO check.", type: "maintenance", status: "completed", serviceDate: days(-48), mileage: 2500, techName: "Marcus Reed", shopName: "Egeland Auto",
  1367. partItems: [{ name: "Engine Oil 15W-40 (5gal)", partNumber: "JD-15W40-5G", quantity: 1, unitPrice: 185, total: 185 }, { name: "Oil Filter", partNumber: "JD-OF-6R", quantity: 1, unitPrice: 32, total: 32 }, { name: "Fuel Filter Kit", partNumber: "JD-FFK-6R", quantity: 1, unitPrice: 68, total: 68 }, { name: "Hydraulic Filter", partNumber: "JD-HF-6R", quantity: 1, unitPrice: 55, total: 55 }, { name: "Air Filter Inner+Outer", partNumber: "JD-AF-6R", quantity: 1, unitPrice: 78, total: 78 }],
  1368. laborItems: [{ description: "Engine oil and all filter replacement", hours: 2.0, rate: 140, total: 280 }, { description: "Hydraulic fluid sampling and analysis", hours: 0.5, rate: 140, total: 70 }, { description: "PTO and 3-point hitch inspection", hours: 1.0, rate: 140, total: 140 }, { description: "Grease all fittings (32 points)", hours: 1.5, rate: 140, total: 210 }],
  1369. subtotal: 1118, taxRate: 8, taxAmount: 89.44, totalAmount: 1207.44 },
  1370. { vehicleId: vehicles[23].id, title: "Fendt 942 - CVT Transmission Service", description: "CVT transmission fluid and filter change. Vario calibration and function test.", type: "maintenance", status: "completed", serviceDate: days(-119), mileage: 1100, techName: "Chris Taylor", shopName: "Egeland Auto",
  1371. partItems: [{ name: "Vario CVT Fluid (10gal)", partNumber: "FENDT-CVT-10G", quantity: 1, unitPrice: 420, total: 420 }, { name: "CVT Filter Kit", partNumber: "FENDT-CVTF-942", quantity: 1, unitPrice: 165, total: 165 }],
  1372. laborItems: [{ description: "CVT transmission fluid drain and refill", hours: 3.0, rate: 150, total: 450 }, { description: "CVT filter replacement", hours: 1.5, rate: 150, total: 225 }, { description: "Vario transmission calibration", hours: 2.0, rate: 150, total: 300 }],
  1373. subtotal: 1560, taxRate: 8, taxAmount: 124.80, totalAmount: 1684.80 },
  1374. { vehicleId: vehicles[29].id, title: "CAT D6 - Hydraulic System Overhaul", description: "Complete hydraulic system service. Replaced main pump seals, flushed system, new filters.", type: "repair", status: "completed", serviceDate: days(-172), mileage: 4000, techName: "Marcus Reed", shopName: "Egeland Auto",
  1375. partItems: [{ name: "Hydraulic Pump Seal Kit", partNumber: "CAT-HPS-D6", quantity: 1, unitPrice: 450, total: 450 }, { name: "Hydraulic Oil (55gal drum)", partNumber: "CAT-HO-55G", quantity: 1, unitPrice: 850, total: 850 }, { name: "Hydraulic Filter Set", partNumber: "CAT-HF-D6", quantity: 2, unitPrice: 120, total: 240 }],
  1376. laborItems: [{ description: "Hydraulic pump disassembly and seal replacement", hours: 6.0, rate: 140, total: 840 }, { description: "System flush and refill", hours: 3.0, rate: 140, total: 420 }, { description: "Pressure testing and calibration", hours: 2.0, rate: 140, total: 280 }],
  1377. subtotal: 3080, taxRate: 8, taxAmount: 246.40, totalAmount: 3326.40 },
  1378. { vehicleId: vehicles[31].id, title: "Komatsu PC210 - Track Chain Replacement", description: "Replaced both track chains and sprockets. Undercarriage inspection completed.", type: "repair", status: "completed", serviceDate: days(-46), mileage: 3000, techName: "Chris Taylor", shopName: "Egeland Auto",
  1379. partItems: [{ name: "Track Chain Assembly (left)", partNumber: "KOM-TC-PC210L", quantity: 1, unitPrice: 2800, total: 2800 }, { name: "Track Chain Assembly (right)", partNumber: "KOM-TC-PC210R", quantity: 1, unitPrice: 2800, total: 2800 }, { name: "Track Sprocket (pair)", partNumber: "KOM-TS-PC210", quantity: 1, unitPrice: 1200, total: 1200 }],
  1380. laborItems: [{ description: "Track chain removal and installation (both sides)", hours: 8.0, rate: 140, total: 1120 }, { description: "Sprocket replacement", hours: 3.0, rate: 140, total: 420 }, { description: "Undercarriage inspection", hours: 2.0, rate: 140, total: 280 }],
  1381. subtotal: 8620, taxRate: 8, taxAmount: 689.60, totalAmount: 9309.60 },
  1382. { vehicleId: vehicles[33].id, title: "Volvo EC220E - 1000hr Full Service", description: "All fluids, filters, track tension, swing bearing inspection.", type: "maintenance", status: "in-progress", serviceDate: days(-12), mileage: 1500, techName: "Chris Taylor", shopName: "Egeland Auto",
  1383. partItems: [{ name: "Engine Oil 15W-40 (5gal)", partNumber: "VOL-15W40-5G", quantity: 1, unitPrice: 185, total: 185 }, { name: "Hydraulic Filter Set", partNumber: "VOL-HFS-EC220", quantity: 1, unitPrice: 120, total: 120 }, { name: "Fuel Filter Kit", partNumber: "VOL-FFK-EC220", quantity: 1, unitPrice: 58, total: 58 }],
  1384. laborItems: [{ description: "Engine oil and all filter replacement", hours: 2.5, rate: 140, total: 350 }, { description: "Track tension adjustment", hours: 1.0, rate: 140, total: 140 }, { description: "Full grease service (48 points)", hours: 2.0, rate: 140, total: 280 }],
  1385. subtotal: 1133, taxRate: 8, taxAmount: 90.64, totalAmount: 1223.64 },
  1386. { vehicleId: vehicles[11].id, title: "Jeep Wrangler - Lift Kit Install", description: "2.5\" suspension lift, new shocks, extended brake lines. Alignment after.", type: "repair", status: "completed", serviceDate: days(-27), mileage: 28000, techName: "Jake Wilson", shopName: "Egeland Auto",
  1387. partItems: [{ name: "Mopar 2.5\" Lift Kit", partNumber: "JEEP-LK-25", quantity: 1, unitPrice: 1200, total: 1200 }, { name: "Bilstein 5100 Shocks (set of 4)", partNumber: "BIL-5100-JL4", quantity: 1, unitPrice: 680, total: 680 }, { name: "Extended Brake Lines", partNumber: "JEEP-EBL-JL", quantity: 1, unitPrice: 120, total: 120 }],
  1388. laborItems: [{ description: "Suspension lift installation", hours: 6.0, rate: 110, total: 660 }, { description: "Brake line routing and bleed", hours: 1.0, rate: 110, total: 110 }, { description: "4-wheel alignment", hours: 1.5, rate: 110, total: 165 }],
  1389. subtotal: 2935, taxRate: 8, taxAmount: 234.80, totalAmount: 3169.80 },
  1390. { vehicleId: vehicles[9].id, title: "Sprinter - Turbocharger Replacement", description: "Turbo shaft play detected. Black smoke under boost. Replaced turbo assembly.", type: "repair", status: "in-progress", serviceDate: days(-18), mileage: 35400, techName: "Chris Taylor", shopName: "Egeland Auto",
  1391. partItems: [{ name: "Turbocharger Assembly - OM651", partNumber: "MB-TURBO-OM651", quantity: 1, unitPrice: 1280, total: 1280 }, { name: "Turbo Oil Feed Line", partNumber: "MB-TOFL-OM651", quantity: 1, unitPrice: 68, total: 68 }, { name: "Turbo Gasket Kit", partNumber: "MB-TGK-OM651", quantity: 1, unitPrice: 42, total: 42 }],
  1392. laborItems: [{ description: "Turbocharger removal", hours: 3.0, rate: 125, total: 375 }, { description: "New turbo installation and oil line", hours: 2.5, rate: 125, total: 312.50 }],
  1393. subtotal: 2077.50, taxRate: 8, taxAmount: 166.20, totalAmount: 2243.70 },
  1394. { vehicleId: vehicles[24].id, title: "Massey Ferguson 8S - Hydraulic Pump", description: "Low hydraulic pressure. Replaced main pump and flushed system.", type: "repair", status: "completed", serviceDate: days(-32), mileage: 3500, techName: "Marcus Reed", shopName: "Egeland Auto",
  1395. partItems: [{ name: "Main Hydraulic Pump Assembly", partNumber: "MF-HPA-8S", quantity: 1, unitPrice: 1650, total: 1650 }, { name: "Hydraulic Oil (15gal)", partNumber: "HYD-ISO46-15G", quantity: 1, unitPrice: 280, total: 280 }, { name: "Hydraulic Filter Set", partNumber: "MF-HFS-8S", quantity: 1, unitPrice: 95, total: 95 }],
  1396. laborItems: [{ description: "Hydraulic pump removal", hours: 4.0, rate: 140, total: 560 }, { description: "New pump installation", hours: 3.0, rate: 140, total: 420 }, { description: "System flush and pressure test", hours: 2.0, rate: 140, total: 280 }],
  1397. subtotal: 3285, taxRate: 8, taxAmount: 262.80, totalAmount: 3547.80 },
  1398. { vehicleId: vehicles[15].id, title: "Ram 1500 - Brake Service", description: "Front and rear brake pad replacement. Rotor resurface. Fluid flush.", type: "maintenance", status: "completed", serviceDate: days(-40), mileage: 19500, techName: "Jake Wilson", shopName: "Egeland Auto",
  1399. partItems: [{ name: "Front Brake Pad Set", partNumber: "RAM-BP-F-DS", quantity: 1, unitPrice: 145, total: 145 }, { name: "Rear Brake Pad Set", partNumber: "RAM-BP-R-DS", quantity: 1, unitPrice: 125, total: 125 }, { name: "Brake Fluid DOT4 (1L)", partNumber: "BF-DOT4-1L", quantity: 2, unitPrice: 18, total: 36 }],
  1400. laborItems: [{ description: "Front brake pad replacement", hours: 1.5, rate: 110, total: 165 }, { description: "Rear brake pad replacement", hours: 1.0, rate: 110, total: 110 }, { description: "Rotor resurface (all 4)", hours: 1.0, rate: 110, total: 110 }, { description: "Brake fluid flush", hours: 0.5, rate: 110, total: 55 }],
  1401. subtotal: 746, taxRate: 8, taxAmount: 59.68, totalAmount: 805.68 },
  1402. { vehicleId: vehicles[14].id, title: "VW Golf GTI - DSG Service + Oil", description: "DSG transmission fluid/filter. Engine oil change. Cabin filter.", type: "maintenance", status: "completed", serviceDate: days(-79), mileage: 38000, techName: "Jake Wilson", shopName: "Egeland Auto",
  1403. partItems: [{ name: "DSG Fluid (7qt)", partNumber: "VW-DSG-7Q", quantity: 1, unitPrice: 145, total: 145 }, { name: "DSG Filter", partNumber: "VW-DSGF", quantity: 1, unitPrice: 52, total: 52 }, { name: "Engine Oil 5W-40 (5qt)", partNumber: "VW-5W40-5Q", quantity: 1, unitPrice: 58, total: 58 }],
  1404. laborItems: [{ description: "DSG fluid and filter change", hours: 1.5, rate: 110, total: 165 }, { description: "Engine oil and filter change", hours: 0.5, rate: 110, total: 55 }],
  1405. subtotal: 475, taxRate: 8, taxAmount: 38, totalAmount: 513 },
  1406. { vehicleId: vehicles[19].id, title: "Mack Granite - PTO Pump Replacement", description: "PTO pump failed. No hydraulic pressure for dump body. Replaced and tested.", type: "repair", status: "completed", serviceDate: days(-71), mileage: 154000, techName: "Marcus Reed", shopName: "Egeland Auto",
  1407. partItems: [{ name: "PTO Hydraulic Pump", partNumber: "MACK-PTO-GR64", quantity: 1, unitPrice: 1450, total: 1450 }, { name: "PTO Mounting Gasket Kit", partNumber: "MACK-PTO-GK", quantity: 1, unitPrice: 85, total: 85 }, { name: "Hydraulic Oil (10gal)", partNumber: "HYD-ISO46-10G", quantity: 1, unitPrice: 180, total: 180 }],
  1408. laborItems: [{ description: "PTO pump removal", hours: 3.0, rate: 140, total: 420 }, { description: "New pump installation", hours: 2.5, rate: 140, total: 350 }, { description: "System prime and dump cycle test", hours: 1.5, rate: 140, total: 210 }],
  1409. subtotal: 2695, taxRate: 8, taxAmount: 215.60, totalAmount: 2910.60 },
  1410. { vehicleId: vehicles[35].id, title: "Case 621G - Bucket Cylinder Reseal", description: "Bucket cylinder leaking. Resealed both cylinders. Tested under load.", type: "repair", status: "completed", serviceDate: days(-101), mileage: 1700, techName: "Chris Taylor", shopName: "Egeland Auto",
  1411. partItems: [{ name: "Bucket Cylinder Seal Kit (pair)", partNumber: "CASE-BCSK-621G", quantity: 1, unitPrice: 380, total: 380 }, { name: "Hydraulic Oil (5gal)", partNumber: "HYD-ISO46-5G", quantity: 2, unitPrice: 95, total: 190 }],
  1412. laborItems: [{ description: "Cylinder removal and disassembly", hours: 3.0, rate: 140, total: 420 }, { description: "Reseal and reassembly", hours: 2.5, rate: 140, total: 350 }, { description: "System bleed and pressure test", hours: 1.5, rate: 140, total: 210 }],
  1413. subtotal: 1550, taxRate: 8, taxAmount: 124, totalAmount: 1674 },
  1414. { vehicleId: vehicles[1].id, title: "F-150 Transmission Fluid Flush", description: "Customer reported rough shifting. Flushed transmission fluid, replaced filter.", type: "repair", status: "completed", serviceDate: days(-36), mileage: 41500, techName: "Jake Wilson", shopName: "Egeland Auto",
  1415. partItems: [{ name: "Mercon ULV ATF (12qt)", partNumber: "FORD-ATF-ULV", quantity: 1, unitPrice: 185, total: 185 }, { name: "Transmission Filter Kit", partNumber: "FORD-TFK-10R80", quantity: 1, unitPrice: 68, total: 68 }],
  1416. laborItems: [{ description: "Transmission fluid flush", hours: 2.0, rate: 110, total: 220 }, { description: "Filter and gasket replacement", hours: 1.5, rate: 110, total: 165 }],
  1417. subtotal: 638, taxRate: 8, taxAmount: 51.04, totalAmount: 689.04 },
  1418. { vehicleId: vehicles[38].id, title: "Corvette C8 - First Annual Service", description: "Annual oil change with performance inspection. Checked brake wear, tire depth, and suspension.", type: "maintenance", status: "completed", serviceDate: days(-46), mileage: 5800, techName: "Chris Taylor", shopName: "Egeland Auto",
  1419. partItems: [{ name: "Mobil 1 0W-40 Full Synthetic (8qt)", partNumber: "CHV-0W40-8Q", quantity: 1, unitPrice: 95, total: 95 }, { name: "Oil Filter (OEM)", partNumber: "CHV-OF-LT2", quantity: 1, unitPrice: 18, total: 18 }, { name: "Cabin Air Filter", partNumber: "CHV-CAF-C8", quantity: 1, unitPrice: 28, total: 28 }],
  1420. laborItems: [{ description: "Oil and filter change (dry sump)", hours: 1.0, rate: 130, total: 130 }, { description: "Performance multi-point inspection", hours: 1.0, rate: 130, total: 130 }],
  1421. subtotal: 401, taxRate: 8, taxAmount: 32.08, totalAmount: 433.08 },
  1422. { vehicleId: vehicles[39].id, title: "Defender 110 - 30K Service + Off-Road Inspection", description: "30K mile service. Oil change, all filters, diff fluids. Extra inspection for off-road wear.", type: "maintenance", status: "completed", serviceDate: days(-89), mileage: 27000, techName: "Erik Haugen", shopName: "Egeland Auto",
  1423. partItems: [{ name: "Engine Oil 0W-20 (7qt)", partNumber: "LR-0W20-7Q", quantity: 1, unitPrice: 78, total: 78 }, { name: "Oil Filter", partNumber: "LR-OF-DEF", quantity: 1, unitPrice: 22, total: 22 }, { name: "Air Filter", partNumber: "LR-AF-DEF", quantity: 1, unitPrice: 45, total: 45 }, { name: "Front Diff Fluid (2qt)", partNumber: "LR-FDF-2Q", quantity: 1, unitPrice: 42, total: 42 }, { name: "Rear Diff Fluid (2.5qt)", partNumber: "LR-RDF-25Q", quantity: 1, unitPrice: 48, total: 48 }],
  1424. laborItems: [{ description: "Oil and filter change", hours: 0.5, rate: 110, total: 55 }, { description: "Air filter replacement", hours: 0.3, rate: 110, total: 33 }, { description: "Front and rear differential fluid change", hours: 1.5, rate: 110, total: 165 }, { description: "Off-road undercarriage inspection", hours: 1.0, rate: 110, total: 110 }],
  1425. subtotal: 598, taxRate: 8, taxAmount: 47.84, totalAmount: 645.84 },
  1426. { vehicleId: vehicles[40].id, title: "Charger R/T - Brake Rotor Replacement", description: "Front rotors warped from aggressive driving. Replace rotors and pads.", type: "repair", status: "completed", serviceDate: days(-32), mileage: 33500, techName: "Jake Wilson", shopName: "Egeland Auto",
  1427. partItems: [{ name: "Front Brake Rotors (pair)", partNumber: "DOD-BR-CHG-F", quantity: 1, unitPrice: 280, total: 280 }, { name: "Front Brake Pad Set (performance)", partNumber: "DOD-BP-CHG-F", quantity: 1, unitPrice: 145, total: 145 }],
  1428. laborItems: [{ description: "Front brake rotor and pad replacement", hours: 2.0, rate: 110, total: 220 }, { description: "Brake system bleed and road test", hours: 0.5, rate: 110, total: 55 }],
  1429. subtotal: 700, taxRate: 8, taxAmount: 56, totalAmount: 756 },
  1430. { vehicleId: vehicles[41].id, title: "WRX - Turbo Boost Leak Repair", description: "Customer reported loss of power under boost. Found cracked intercooler pipe.", type: "repair", status: "completed", serviceDate: days(-38), mileage: 11800, techName: "Chris Taylor", shopName: "Egeland Auto",
  1431. partItems: [{ name: "Intercooler Pipe + Couplers", partNumber: "SUB-ICP-WRX", quantity: 1, unitPrice: 165, total: 165 }, { name: "T-Bolt Clamps (set of 4)", partNumber: "SUB-TBC-4", quantity: 1, unitPrice: 28, total: 28 }],
  1432. laborItems: [{ description: "Boost leak diagnosis and smoke test", hours: 1.0, rate: 120, total: 120 }, { description: "Intercooler pipe replacement", hours: 1.5, rate: 120, total: 180 }],
  1433. subtotal: 493, taxRate: 8, taxAmount: 39.44, totalAmount: 532.44 },
  1434. ] satisfies Array<{ status: JobStatus } & Record<string, unknown>>;
  1435. const serviceRecords = [];
  1436. for (const sr of svcData) {
  1437. const { partItems, laborItems, ...data } = sr;
  1438. const record = await prisma.serviceRecord.create({ data: { ...data, organizationId: ORG_ID, cost: data.totalAmount, partItems: { create: partItems }, laborItems: { create: laborItems } } });
  1439. serviceRecords.push(record);
  1440. }
  1441. console.log(` Created ${serviceRecords.length} service records`);
  1442. // -- Quotes (15) --
  1443. console.log("\nCreating quotes...");
  1444. const qData = [
  1445. { quoteNumber: `Q-${YEAR}-001`, title: "Full Brake Overhaul - BMW 330i", status: "sent", validUntil: days(6), customerId: customers[3].id, vehicleId: vehicles[4].id,
  1446. partItems: [{ name: "Front Brake Rotor Set (OEM)", partNumber: "BMW-BR-F34-OEM", quantity: 1, unitPrice: 480, total: 480 }, { name: "Rear Brake Rotor Set (OEM)", partNumber: "BMW-BR-R34-OEM", quantity: 1, unitPrice: 390, total: 390 }, { name: "Front Brake Pad Set (OEM)", partNumber: "BMW-BP-F34-OEM", quantity: 1, unitPrice: 220, total: 220 }],
  1447. laborItems: [{ description: "Front brake rotor and pad replacement", hours: 2.5, rate: 110, total: 275 }, { description: "Rear brake rotor replacement", hours: 2.0, rate: 110, total: 220 }],
  1448. subtotal: 1585, taxRate: 8, taxAmount: 126.80, totalAmount: 1711.80 },
  1449. { quoteNumber: `Q-${YEAR}-002`, title: "200K Engine Service - Kenworth T680", status: "accepted", validUntil: days(-8), customerId: customers[2].id, vehicleId: vehicles[17].id,
  1450. partItems: [{ name: "PACCAR MX-13 Overhaul Kit", partNumber: "PAC-OHK-MX13", quantity: 1, unitPrice: 850, total: 850 }, { name: "Injector Set (6)", partNumber: "PAC-INJ-MX13-6", quantity: 1, unitPrice: 1420, total: 1420 }],
  1451. laborItems: [{ description: "Injector replacement (6 cylinders)", hours: 8.0, rate: 140, total: 1120 }, { description: "Valve adjustment", hours: 4.0, rate: 140, total: 560 }],
  1452. subtotal: 3950, taxRate: 8, taxAmount: 316, totalAmount: 4266 },
  1453. { quoteNumber: `Q-${YEAR}-003`, title: "CAT D6 Undercarriage Rebuild", status: "sent", validUntil: days(21), customerId: customers[0].id, vehicleId: vehicles[29].id,
  1454. partItems: [{ name: "Track Chain Assembly (pair)", partNumber: "CAT-TCA-D6", quantity: 1, unitPrice: 6500, total: 6500 }, { name: "Track Roller Set (14 pcs)", partNumber: "CAT-TR-D6-14", quantity: 1, unitPrice: 4200, total: 4200 }, { name: "Sprocket Set (pair)", partNumber: "CAT-SS-D6", quantity: 1, unitPrice: 1500, total: 1500 }],
  1455. laborItems: [{ description: "Complete undercarriage disassembly", hours: 12.0, rate: 140, total: 1680 }, { description: "Component installation", hours: 16.0, rate: 140, total: 2240 }],
  1456. subtotal: 16120, taxRate: 8, taxAmount: 1289.60, totalAmount: 17409.60 },
  1457. { quoteNumber: `Q-${YEAR}-004`, title: "John Deere 6R - Front Axle Overhaul", status: "sent", validUntil: days(37), customerId: customers[10].id, vehicleId: vehicles[22].id,
  1458. partItems: [{ name: "Front Axle Bearing Kit", partNumber: "JD-FABK-6R", quantity: 1, unitPrice: 520, total: 520 }, { name: "CV Joint Assembly (pair)", partNumber: "JD-CVJ-6R", quantity: 1, unitPrice: 850, total: 850 }],
  1459. laborItems: [{ description: "Front axle removal", hours: 5.0, rate: 140, total: 700 }, { description: "Bearing and CV replacement", hours: 4.0, rate: 140, total: 560 }],
  1460. subtotal: 2630, taxRate: 8, taxAmount: 210.40, totalAmount: 2840.40 },
  1461. { quoteNumber: `Q-${YEAR}-005`, title: "Honda Civic - Clutch Replacement", status: "accepted", validUntil: days(1), customerId: customers[9].id, vehicleId: vehicles[5].id,
  1462. partItems: [{ name: "Clutch Kit (disc, plate, bearing)", partNumber: "HON-CLK-FK7", quantity: 1, unitPrice: 380, total: 380 }],
  1463. laborItems: [{ description: "Transmission removal and clutch replacement", hours: 5.0, rate: 110, total: 550 }, { description: "Reinstallation and test drive", hours: 2.0, rate: 110, total: 220 }],
  1464. subtotal: 1150, taxRate: 8, taxAmount: 92, totalAmount: 1242 },
  1465. { quoteNumber: `Q-${YEAR}-006`, title: "Volvo FH 640 - Air Suspension Repair", status: "accepted", validUntil: days(1), customerId: customers[2].id, vehicleId: vehicles[18].id,
  1466. partItems: [{ name: "Rear Air Spring (pair)", partNumber: "VOL-RAS-FH", quantity: 1, unitPrice: 560, total: 560 }, { name: "Height Sensor", partNumber: "VOL-HS-FH", quantity: 1, unitPrice: 240, total: 240 }],
  1467. laborItems: [{ description: "Air bag replacement (both sides)", hours: 3.0, rate: 140, total: 420 }, { description: "Sensor replacement and calibration", hours: 1.5, rate: 140, total: 210 }],
  1468. subtotal: 1430, taxRate: 8, taxAmount: 114.40, totalAmount: 1544.40 },
  1469. { quoteNumber: `Q-${YEAR}-007`, title: "Fendt 942 - A/C Compressor Repair", status: "draft", validUntil: days(32), customerId: customers[18].id, vehicleId: vehicles[23].id,
  1470. partItems: [{ name: "A/C Compressor - Fendt 942", partNumber: "FENDT-ACC-942", quantity: 1, unitPrice: 890, total: 890 }, { name: "Condenser Assembly", partNumber: "FENDT-COND-942", quantity: 1, unitPrice: 450, total: 450 }],
  1471. laborItems: [{ description: "Cab disassembly for A/C access", hours: 3.0, rate: 150, total: 450 }, { description: "Compressor and condenser replacement", hours: 4.0, rate: 150, total: 600 }],
  1472. subtotal: 2390, taxRate: 8, taxAmount: 191.20, totalAmount: 2581.20 },
  1473. { quoteNumber: `Q-${YEAR}-008`, title: "Komatsu PC210 - Boom Cylinder Reseal", status: "sent", validUntil: days(16), customerId: customers[8].id, vehicleId: vehicles[31].id,
  1474. partItems: [{ name: "Boom Cylinder Seal Kit (pair)", partNumber: "KOM-BCSK-PC210", quantity: 1, unitPrice: 520, total: 520 }, { name: "Hydraulic Oil (10gal)", partNumber: "HYD-ISO46-10G", quantity: 1, unitPrice: 180, total: 180 }],
  1475. laborItems: [{ description: "Boom cylinder removal", hours: 4.0, rate: 140, total: 560 }, { description: "Disassembly and reseal", hours: 3.0, rate: 140, total: 420 }],
  1476. subtotal: 1680, taxRate: 8, taxAmount: 134.40, totalAmount: 1814.40 },
  1477. { quoteNumber: `Q-${YEAR}-009`, title: "Liebherr Crane - Annual Certification", status: "sent", validUntil: days(53), customerId: customers[14].id, vehicleId: vehicles[36].id,
  1478. partItems: [{ name: "Wire Rope Inspection Kit", partNumber: "LBH-WRIK", quantity: 1, unitPrice: 250, total: 250 }, { name: "Hydraulic Hose Set (safety critical)", partNumber: "LBH-HHS-SC", quantity: 1, unitPrice: 850, total: 850 }],
  1479. laborItems: [{ description: "Full crane inspection per OSHA", hours: 8.0, rate: 160, total: 1280 }, { description: "Hydraulic hose replacement", hours: 4.0, rate: 160, total: 640 }, { description: "Load test and certification", hours: 4.0, rate: 160, total: 640 }],
  1480. subtotal: 3660, taxRate: 8, taxAmount: 292.80, totalAmount: 3952.80 },
  1481. { quoteNumber: `Q-${YEAR}-010`, title: "Porsche 911 - Suspension Refresh", status: "draft", validUntil: days(23), customerId: customers[3].id, vehicleId: vehicles[13].id,
  1482. partItems: [{ name: "Bilstein B8 Dampers (set of 4)", partNumber: "POR-BIL-B8-4", quantity: 1, unitPrice: 1800, total: 1800 }, { name: "H&R Sport Springs", partNumber: "POR-HR-SS-992", quantity: 1, unitPrice: 650, total: 650 }],
  1483. laborItems: [{ description: "Suspension removal and install", hours: 5.0, rate: 145, total: 725 }, { description: "4-corner alignment", hours: 1.5, rate: 145, total: 217.50 }],
  1484. subtotal: 3392.50, taxRate: 8, taxAmount: 271.40, totalAmount: 3663.90 },
  1485. { quoteNumber: `Q-${YEAR}-011`, title: "Jeep Wrangler - Winch Install", status: "accepted", validUntil: days(11), customerId: customers[11].id, vehicleId: vehicles[11].id,
  1486. partItems: [{ name: "Warn VR EVO 10-S Winch", partNumber: "WARN-VR10S", quantity: 1, unitPrice: 850, total: 850 }, { name: "Winch Mounting Plate", partNumber: "JEEP-WMP-JL", quantity: 1, unitPrice: 220, total: 220 }],
  1487. laborItems: [{ description: "Winch and plate installation", hours: 3.0, rate: 110, total: 330 }, { description: "Wiring and relay setup", hours: 1.5, rate: 110, total: 165 }],
  1488. subtotal: 1565, taxRate: 8, taxAmount: 125.20, totalAmount: 1690.20 },
  1489. { quoteNumber: `Q-${YEAR}-012`, title: "Fleet Oil Change Package - 10 Trucks", status: "sent", validUntil: days(6), customerId: customers[16].id, notes: "Bulk fleet pricing. Drop off in batches of 3-4.",
  1490. partItems: [{ name: "Synthetic Oil 5W-30 (5qt) x10", partNumber: "OIL-5W30-BULK", quantity: 10, unitPrice: 38, total: 380 }, { name: "Oil Filter Assorted x10", partNumber: "OF-ASSORTED-10", quantity: 10, unitPrice: 9.50, total: 95 }],
  1491. laborItems: [{ description: "Oil change service x10 vehicles", hours: 5.0, rate: 85, total: 425 }],
  1492. subtotal: 900, taxRate: 8, taxAmount: 72, totalAmount: 972 },
  1493. { quoteNumber: `Q-${YEAR}-013`, title: "S780 Combine - Pre-Season Check", status: "accepted", validUntil: days(37), customerId: customers[18].id, vehicleId: vehicles[27].id,
  1494. partItems: [{ name: "Combine Service Kit (all filters)", partNumber: "JD-CSK-S780", quantity: 1, unitPrice: 420, total: 420 }, { name: "Sickle Blade Set", partNumber: "JD-SBS-S780", quantity: 1, unitPrice: 380, total: 380 }, { name: "Feeder Chain", partNumber: "JD-FC-S780", quantity: 1, unitPrice: 650, total: 650 }],
  1495. laborItems: [{ description: "Full filter service and oil change", hours: 3.0, rate: 140, total: 420 }, { description: "Sickle replacement and header setup", hours: 4.0, rate: 140, total: 560 }, { description: "Feeder chain replacement", hours: 3.0, rate: 140, total: 420 }],
  1496. subtotal: 2850, taxRate: 8, taxAmount: 228, totalAmount: 3078 },
  1497. { quoteNumber: `Q-${YEAR}-015`, title: "Toyota Tacoma - Suspension Upgrade", status: "sent", validUntil: days(23), customerId: customers[19].id, vehicleId: vehicles[16].id,
  1498. partItems: [{ name: "Bilstein 5100 Front (pair)", partNumber: "BIL-5100-TAC-F", quantity: 1, unitPrice: 340, total: 340 }, { name: "Bilstein 5100 Rear (pair)", partNumber: "BIL-5100-TAC-R", quantity: 1, unitPrice: 280, total: 280 }, { name: "OME Leaf Springs", partNumber: "OME-LS-TAC", quantity: 1, unitPrice: 450, total: 450 }],
  1499. laborItems: [{ description: "Front strut replacement", hours: 2.5, rate: 110, total: 275 }, { description: "Rear shock and leaf spring install", hours: 3.0, rate: 110, total: 330 }, { description: "Alignment", hours: 1.0, rate: 110, total: 110 }],
  1500. subtotal: 1785, taxRate: 8, taxAmount: 142.80, totalAmount: 1927.80 },
  1501. ];
  1502. const quotes = [];
  1503. for (const q of qData) {
  1504. const { partItems, laborItems, customerId, vehicleId, ...fields } = q;
  1505. const quote = await prisma.quote.create({ data: { ...fields, userId: USER_ID, organizationId: ORG_ID, ...warrantyColumns(null), ...(customerId ? { customerId } : {}), ...(vehicleId ? { vehicleId } : {}), partItems: { create: partItems }, laborItems: { create: laborItems } } });
  1506. quotes.push(quote);
  1507. }
  1508. console.log(` Created ${quotes.length} quotes`);
  1509. // -- Inventory Parts (25) --
  1510. console.log("\nCreating inventory parts...");
  1511. const invData = [
  1512. { name: "Synthetic Engine Oil 5W-30 (5qt)", partNumber: "OIL-5W30-5Q", category: "Fluids & Lubricants", quantity: 24, minQuantity: 10, unit: "pcs", unitCost: 28, sellPrice: 45, supplier: "AutoZone Commercial", location: "Shelf A-1" },
  1513. { name: "Synthetic Engine Oil 0W-20 (5qt)", partNumber: "OIL-0W20-5Q", category: "Fluids & Lubricants", quantity: 18, minQuantity: 8, unit: "pcs", unitCost: 31, sellPrice: 49, supplier: "AutoZone Commercial", location: "Shelf A-1" },
  1514. { name: "Heavy Duty Engine Oil 15W-40 (5gal)", partNumber: "OIL-15W40-5G", category: "Fluids & Lubricants", quantity: 8, minQuantity: 4, unit: "pcs", unitCost: 120, sellPrice: 185, supplier: "Shell Fleet", location: "Shelf A-2" },
  1515. { name: "Hydraulic Oil ISO 46 (55gal)", partNumber: "HYD-ISO46-55G", category: "Fluids & Lubricants", quantity: 3, minQuantity: 2, unit: "pcs", unitCost: 580, sellPrice: 850, supplier: "Shell Fleet", location: "Floor A-3" },
  1516. { name: "Brake Pad Set - Universal Light Vehicle", partNumber: "BP-UNI-LV", category: "Brakes", quantity: 14, minQuantity: 6, unit: "pcs", unitCost: 38, sellPrice: 68, supplier: "NAPA Auto Parts", location: "Shelf B-1" },
  1517. { name: "Brake Pad Set - Heavy Duty Truck", partNumber: "BP-HD-TRUCK", category: "Brakes", quantity: 6, minQuantity: 4, unit: "pcs", unitCost: 180, sellPrice: 320, supplier: "FleetPride", location: "Shelf B-2" },
  1518. { name: "Oil Filter - European Vehicles", partNumber: "OF-EUR-01", category: "Filters", quantity: 30, minQuantity: 10, unit: "pcs", unitCost: 6.50, sellPrice: 15.50, supplier: "Mann-Filter USA", location: "Shelf C-1" },
  1519. { name: "Oil Filter - Asian Vehicles", partNumber: "OF-ASN-01", category: "Filters", quantity: 25, minQuantity: 10, unit: "pcs", unitCost: 4.50, sellPrice: 12, supplier: "Mann-Filter USA", location: "Shelf C-1" },
  1520. { name: "Hydraulic Filter - CAT/Komatsu", partNumber: "HF-CAT-KOM", category: "Filters", quantity: 5, minQuantity: 3, unit: "pcs", unitCost: 65, sellPrice: 120, supplier: "Cat Parts Direct", location: "Shelf C-3" },
  1521. { name: "Spark Plug - Iridium (each)", partNumber: "SP-IRID-01", category: "Ignition", quantity: 40, minQuantity: 16, unit: "pcs", unitCost: 9.50, sellPrice: 17.50, supplier: "NGK USA", location: "Shelf D-1" },
  1522. { name: "Car Battery 12V 700CCA", partNumber: "BAT-12V-700CCA", category: "Electrical", quantity: 6, minQuantity: 3, unit: "pcs", unitCost: 95, sellPrice: 165, supplier: "Interstate Batteries", location: "Floor D-2" },
  1523. { name: "Truck Battery 12V 1000CCA", partNumber: "BAT-12V-1000CCA", category: "Electrical", quantity: 4, minQuantity: 2, unit: "pcs", unitCost: 240, sellPrice: 380, supplier: "Interstate Batteries", location: "Floor D-2" },
  1524. { name: "Coolant Concentrate (1gal)", partNumber: "COOL-CONC-1G", category: "Fluids & Lubricants", quantity: 15, minQuantity: 6, unit: "pcs", unitCost: 18, sellPrice: 32, supplier: "AutoZone Commercial", location: "Shelf A-2" },
  1525. { name: "ATF Transmission Fluid (1qt)", partNumber: "ATF-DEXVI-1Q", category: "Fluids & Lubricants", quantity: 20, minQuantity: 8, unit: "pcs", unitCost: 12, sellPrice: 22, supplier: "AutoZone Commercial", location: "Shelf A-2" },
  1526. { name: "Serpentine Belt - Universal", partNumber: "SB-UNI-6PK", category: "Engine", quantity: 8, minQuantity: 4, unit: "pcs", unitCost: 22, sellPrice: 42, supplier: "Gates Corp", location: "Shelf D-3" },
  1527. { name: "Hydraulic Hose - 1/2\" x 10ft", partNumber: "HH-12-10FT", category: "Hydraulics", quantity: 10, minQuantity: 4, unit: "pcs", unitCost: 45, sellPrice: 85, supplier: "Parker Hannifin", location: "Rack G-1" },
  1528. { name: "Track Shoe Bolt Kit (set of 50)", partNumber: "TSB-KIT-50", category: "Undercarriage", quantity: 4, minQuantity: 2, unit: "pcs", unitCost: 180, sellPrice: 320, supplier: "Cat Parts Direct", location: "Floor G-2" },
  1529. { name: "R-134a Refrigerant (30oz can)", partNumber: "R134A-30OZ", category: "HVAC", quantity: 8, minQuantity: 4, unit: "pcs", unitCost: 18, sellPrice: 32, supplier: "AutoZone Commercial", location: "Shelf H-1" },
  1530. { name: "Fuel Filter - Diesel Universal", partNumber: "FF-DSL-UNI", category: "Filters", quantity: 12, minQuantity: 5, unit: "pcs", unitCost: 14, sellPrice: 29, supplier: "Mann-Filter USA", location: "Shelf C-2" },
  1531. { name: "Air Filter - Heavy Equipment", partNumber: "AF-HE-UNI", category: "Filters", quantity: 6, minQuantity: 3, unit: "pcs", unitCost: 48, sellPrice: 85, supplier: "Cat Parts Direct", location: "Shelf C-3" },
  1532. { name: "Tractor PTO Shaft Guard", partNumber: "PTO-SG-UNI", category: "Safety", quantity: 3, minQuantity: 2, unit: "pcs", unitCost: 85, sellPrice: 150, supplier: "Agri Supply Co.", location: "Rack G-3" },
  1533. { name: "DPF Pressure Sensor", partNumber: "DPF-PS-UNI", category: "Emissions", quantity: 1, minQuantity: 3, unit: "pcs", unitCost: 65, sellPrice: 120, supplier: "Bosch USA", location: "Shelf D-5" },
  1534. { name: "Grease Cartridge EP2 (box of 10)", partNumber: "GREASE-EP2-10", category: "Fluids & Lubricants", quantity: 12, minQuantity: 5, unit: "pcs", unitCost: 28, sellPrice: 48, supplier: "Shell Fleet", location: "Shelf A-3" },
  1535. { name: "Combine Sickle Section (box of 25)", partNumber: "CSS-UNI-25", category: "Harvest Parts", quantity: 4, minQuantity: 2, unit: "pcs", unitCost: 65, sellPrice: 110, supplier: "Agri Supply Co.", location: "Rack G-4" },
  1536. { name: "Wiper Blade 24\" Universal", partNumber: "WB-24-UNI", category: "Body & Exterior", quantity: 12, minQuantity: 6, unit: "pcs", unitCost: 11, sellPrice: 25, supplier: "Bosch USA", location: "Shelf E-1" },
  1537. // Bulk stock measured by volume and weight - shows units of measure and
  1538. // fractional quantities off (booking 1.4 lb deducts exactly 1.4).
  1539. { name: "Bulk Synthetic Oil 5W-30", partNumber: "OIL-5W30-BULK", category: "Fluids & Lubricants", quantity: 38.5, minQuantity: 12, unit: "qt", unitCost: 4.80, sellPrice: 8.50, supplier: "Shell Fleet", location: "Tank A-4" },
  1540. { name: "Refrigerant R-1234yf (bulk)", partNumber: "R1234YF-BULK", category: "HVAC", quantity: 12.4, minQuantity: 4, unit: "lb", unitCost: 38, sellPrice: 68, supplier: "AutoZone Commercial", location: "Cage H-2" },
  1541. { name: "Windshield Washer Fluid (bulk)", partNumber: "WWF-BULK", category: "Fluids & Lubricants", quantity: 27.5, minQuantity: 10, unit: "gal", unitCost: 2.20, sellPrice: 5, supplier: "AutoZone Commercial", location: "Tank A-5" },
  1542. ];
  1543. const inventoryParts = await Promise.all(invData.map(p => prisma.inventoryPart.create({ data: { ...p, userId: USER_ID, organizationId: ORG_ID } })));
  1544. console.log(` Created ${inventoryParts.length} inventory parts`);
  1545. // -- Fuel Logs --
  1546. console.log("\nCreating fuel logs...");
  1547. await Promise.all([
  1548. prisma.fuelLog.create({ data: { vehicleId: vehicles[1].id, date: days(-63), mileage: 40200, gallons: 26, pricePerGallon: 3.45, totalCost: 89.70, station: "Shell - I-25 & Hampden" } }),
  1549. prisma.fuelLog.create({ data: { vehicleId: vehicles[1].id, date: days(-35), mileage: 41500, gallons: 24, pricePerGallon: 3.48, totalCost: 83.52, station: "Costco Gas - Arvada" } }),
  1550. prisma.fuelLog.create({ data: { vehicleId: vehicles[17].id, date: days(-58), mileage: 182000, gallons: 120, pricePerGallon: 3.85, totalCost: 462.00, station: "Pilot Travel Center - Portland" } }),
  1551. prisma.fuelLog.create({ data: { vehicleId: vehicles[8].id, date: days(-27), mileage: 98000, gallons: 28, pricePerGallon: 3.65, totalCost: 102.20, station: "Buc-ee's - San Antonio" } }),
  1552. prisma.fuelLog.create({ data: { vehicleId: vehicles[22].id, date: days(-22), mileage: 2700, gallons: 35, pricePerGallon: 3.78, totalCost: 132.30, station: "Co-op Fuel - Omaha" } }),
  1553. prisma.fuelLog.create({ data: { vehicleId: vehicles[23].id, date: days(-40), mileage: 1180, gallons: 80, pricePerGallon: 3.82, totalCost: 305.60, station: "Farm Bureau Fuel - Des Moines" } }),
  1554. prisma.fuelLog.create({ data: { vehicleId: vehicles[15].id, date: days(-17), mileage: 19500, gallons: 22, pricePerGallon: 3.55, totalCost: 78.10, station: "Speedway - Detroit" } }),
  1555. prisma.fuelLog.create({ data: { vehicleId: vehicles[11].id, date: days(-46), mileage: 28200, gallons: 18, pricePerGallon: 3.62, totalCost: 65.16, station: "Shell - I-70 & Morrison" } }),
  1556. ]);
  1557. console.log(" Created 8 fuel logs");
  1558. // -- Reminders --
  1559. // Spread deliberately around today: a few overdue, several due today and this
  1560. // week, then a tail running out a few months. That is what makes the reminders
  1561. // page, the dashboard counters and the calendar all look alive at once.
  1562. // organizationId is required - getReminders() and the calendar both scope by
  1563. // it, so a reminder without one is invisible in the app.
  1564. console.log("\nCreating reminders...");
  1565. const coreReminders = await Promise.all([
  1566. // Overdue - the cron has already notified on the older ones
  1567. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[17].id, title: "Annual DOT Inspection", description: "Kenworth T680 - annual inspection is past due, Pacific Freight notified twice", dueDate: days(-12), notifyInApp: true, notifyEmail: true, notifiedAt: days(-12) } }),
  1568. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[5].id, title: "CV boot replacement", description: "Torn boot found at last service. Customer has not booked yet.", dueDate: days(-8), notifyInApp: true, notifiedAt: days(-8) } }),
  1569. prisma.reminder.create({ data: { organizationId: ORG_ID, customerId: customers[16].id, title: "Chase fleet PM schedule", description: "Greenfield Waste has not confirmed the quarterly PM slots for 12 trucks.", dueDate: days(-5), notifyInApp: true, notifiedAt: days(-5) } }),
  1570. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[8].id, title: "Battery replacement", description: "Load test showed 78% capacity. Replace before winter.", dueDate: days(-3), notifyInApp: true } }),
  1571. prisma.reminder.create({ data: { organizationId: ORG_ID, title: "Order brake pad stock", description: "Heavy duty truck pads down to 6 sets. Reorder from FleetPride.", dueDate: days(-1), notifyInApp: true } }),
  1572. // Due today
  1573. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[18].id, title: "Volvo FH 640 - brake inspection", description: "Check brakes before the next long-haul run to Chicago.", dueDate: days(0), notifyInApp: true, notifyEmail: true } }),
  1574. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[9].id, title: "Call Kevin about the Sprinter", description: "Turbo job finished - confirm pickup time before his catering event.", dueDate: days(0), notifyInApp: true } }),
  1575. prisma.reminder.create({ data: { organizationId: ORG_ID, title: "Weekly parts order cut-off", description: "Submit the NAPA order before 15:00 or it ships Monday.", dueDate: days(0), notifyInApp: true } }),
  1576. // This week
  1577. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[0].id, title: "Next oil change", description: "Due at 25,000 mi. Customer wants Mobil 1 0W-20.", dueDate: days(1), dueMileage: 25000, notifyInApp: true } }),
  1578. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[31].id, title: "Order boom cylinder seal kit", description: "Komatsu PC210 reseal cannot start until the kit lands.", dueDate: days(2), notifyInApp: true } }),
  1579. prisma.reminder.create({ data: { organizationId: ORG_ID, customerId: customers[0].id, title: "Summit Construction - Net-30 invoices", description: "Three invoices approaching terms. Send a statement.", dueDate: days(3), notifyInApp: true, notifyEmail: true } }),
  1580. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[36].id, title: "Book crane load test", description: "Liebherr LTM 1100 annual certification needs an external inspector.", dueDate: days(4), notifyInApp: true } }),
  1581. prisma.reminder.create({ data: { organizationId: ORG_ID, title: "Calibrate brake tester", description: "Roller brake tester calibration certificate expires this month.", dueDate: days(5), notifyInApp: true, notifyEmail: true } }),
  1582. // Next two weeks
  1583. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[13].id, title: "Porsche 911 - track day prep", description: "Brake pads, fluid and tire pressure before the customer's HPDE weekend.", dueDate: days(9), notifyInApp: true } }),
  1584. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[24].id, title: "Massey Ferguson - radiator deep clean", description: "Chaff blockage caused overheating. Pull the radiator when the tractor is back.", dueDate: days(11), notifyInApp: true } }),
  1585. prisma.reminder.create({ data: { organizationId: ORG_ID, customerId: customers[10].id, title: "Prairie Farms - pre-planting check-in", description: "Book both tractors before the seasonal rush fills the board.", dueDate: days(13), notifyInApp: true } }),
  1586. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[19].id, title: "Dump body weld repair", description: "Stress cracks near the rear hinge. Do not let it run past 10,000 more miles.", dueDate: days(16), notifyInApp: true } }),
  1587. prisma.reminder.create({ data: { organizationId: ORG_ID, title: "Shop lift annual inspection", description: "Both two-post lifts are due for their yearly certification.", dueDate: days(18), notifyInApp: true, notifyEmail: true } }),
  1588. // Later
  1589. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[23].id, title: "Fendt 942 - season prep", description: "Full check before the field season starts.", dueDate: days(26), notifyInApp: true } }),
  1590. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[4].id, title: "Brake fluid flush", description: "BMW recommends every 2 years.", dueDate: days(48), notifyInApp: true } }),
  1591. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[27].id, title: "S780 combine - pre-harvest inspection", description: "Critical unit. Everything must be done before harvest starts.", dueDate: days(72), notifyInApp: true, notifyEmail: true } }),
  1592. prisma.reminder.create({ data: { organizationId: ORG_ID, customerId: customers[14].id, title: "Titan Building - contract renewal", description: "Annual maintenance agreement for 20+ machines comes up for renewal.", dueDate: days(95), notifyInApp: true } }),
  1593. // Mileage/hour based, no date
  1594. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[29].id, title: "Track tension check", description: "CAT D6 - check after 100 hours.", dueMileage: 4500, notifyInApp: true } }),
  1595. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[22].id, title: "1000hr service", description: "John Deere 6R 250 - scheduled interval service.", dueMileage: 3500, notifyInApp: true } }),
  1596. // Already handled
  1597. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[11].id, title: "Post-lift alignment check", description: "Done at the same visit as the lift kit install.", dueDate: days(-20), isCompleted: true } }),
  1598. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[15].id, title: "Brake service follow-up", description: "Customer confirmed no more noise after the pad replacement.", dueDate: days(-14), isCompleted: true } }),
  1599. prisma.reminder.create({ data: { organizationId: ORG_ID, customerId: customers[3].id, title: "Send Sarah the BMW brake quote", description: "Quote sent and accepted.", dueDate: days(-6), isCompleted: true } }),
  1600. ]);
  1601. console.log(` Created ${coreReminders.length} reminders`);
  1602. // -- Vehicle Notes --
  1603. console.log("\nCreating vehicle notes...");
  1604. await Promise.all([
  1605. prisma.note.create({ data: { vehicleId: vehicles[4].id, title: "Customer preference", content: "Sarah always requests OEM BMW parts. No aftermarket. She prefers to wait while service is done.", isPinned: true } }),
  1606. prisma.note.create({ data: { vehicleId: vehicles[29].id, title: "Hydraulic leak history", content: "This D6 has had recurring hydraulic leaks on the left blade cylinder. Last repaired Sept 2025. Monitor closely.", isPinned: true } }),
  1607. prisma.note.create({ data: { vehicleId: vehicles[17].id, title: "Fleet note", content: "Pacific Freight unit #3. Driver: Tom Bradley. Call dispatch at (555) 302-5566 for scheduling.", isPinned: false } }),
  1608. prisma.note.create({ data: { vehicleId: vehicles[6].id, title: "Charging note", content: "Customer reports occasional slow charging at home. Suggested checking home charger firmware. Not a vehicle issue.", isPinned: false } }),
  1609. prisma.note.create({ data: { vehicleId: vehicles[36].id, title: "Outrigger issue", content: "Right rear outrigger cylinder slow to extend. May need seal replacement at next service.", isPinned: true } }),
  1610. prisma.note.create({ data: { vehicleId: vehicles[22].id, title: "Warranty info", content: "John Deere 6R 250 still under manufacturer warranty until March 2027. Contact JD dealer for warranty claims.", isPinned: true } }),
  1611. prisma.note.create({ data: { vehicleId: vehicles[23].id, title: "Operator preference", content: "Fendt 942 - Operator prefers front linkage set to position control, not draft. Do not change settings after service.", isPinned: true } }),
  1612. prisma.note.create({ data: { vehicleId: vehicles[18].id, title: "Volvo FH 640 fleet note", content: "Pacific Freight unit #5. Long-haul Portland to Chicago route. D13TC engine with I-Shift dual clutch.", isPinned: true } }),
  1613. prisma.note.create({ data: { vehicleId: vehicles[11].id, title: "Lift kit installed", content: "2.5\" Mopar lift with Bilstein 5100 shocks installed Feb 2026. Customer plans to add 35\" tires next.", isPinned: false } }),
  1614. ]);
  1615. console.log(" Created 9 vehicle notes");
  1616. // -- Additional vehicle notes (customer preferences, reminders, history) --
  1617. console.log("\nCreating additional vehicle notes...");
  1618. const additionalNotes = await Promise.all([
  1619. prisma.note.create({ data: { vehicleId: vehicles[0].id, title: "Customer preference", content: "James prefers Mobil 1 full synthetic 0W-20 on every oil change. Do not substitute.", isPinned: true } }),
  1620. prisma.note.create({ data: { vehicleId: vehicles[0].id, title: "Key location", content: "Spare key in lockbox #4 (code 4412). Customer drops off after hours sometimes.", isPinned: false } }),
  1621. prisma.note.create({ data: { vehicleId: vehicles[1].id, title: "Fleet maintenance policy", content: "Summit Construction requires photo documentation of all brake and tire work. Upload to service record.", isPinned: true } }),
  1622. prisma.note.create({ data: { vehicleId: vehicles[1].id, title: "Aftermarket bed liner", content: "Spray-in bed liner installed 2023. Don't pressure wash interior of bed at close range.", isPinned: false } }),
  1623. prisma.note.create({ data: { vehicleId: vehicles[2].id, title: "Off-road use", content: "Mike uses this for hunting trips. Undercarriage gets a lot of mud - always inspect skid plates.", isPinned: false } }),
  1624. prisma.note.create({ data: { vehicleId: vehicles[3].id, title: "Municipal contract vehicle", content: "Metro City Services - invoicing via purchase order only. PO required before work starts.", isPinned: true } }),
  1625. prisma.note.create({ data: { vehicleId: vehicles[5].id, title: "Budget-conscious customer", content: "Amanda prefers aftermarket parts where safe. Always provide OEM vs aftermarket pricing options.", isPinned: true } }),
  1626. prisma.note.create({ data: { vehicleId: vehicles[5].id, title: "Check AC vents", content: "Musty smell reported last visit. Recommended cabin filter + evaporator clean at next service.", isPinned: false } }),
  1627. prisma.note.create({ data: { vehicleId: vehicles[7].id, title: "Referral source", content: "Jessica was referred by Ryan Parker. Offer referral discount on next service.", isPinned: false } }),
  1628. prisma.note.create({ data: { vehicleId: vehicles[8].id, title: "Ranch use", content: "Silverado used for cattle hauling. Check hitch and trailer plug every visit.", isPinned: false } }),
  1629. prisma.note.create({ data: { vehicleId: vehicles[9].id, title: "Commercial use", content: "Kevin uses this daily for catering. Needs quick turnaround - try to get him in/out same day.", isPinned: true } }),
  1630. prisma.note.create({ data: { vehicleId: vehicles[10].id, title: "OEM parts only", content: "David insists on genuine Audi parts. No VAG knockoffs. He'll ask to see part numbers.", isPinned: true } }),
  1631. prisma.note.create({ data: { vehicleId: vehicles[12].id, title: "Soft top care", content: "Customer has both hard and soft top. Store removed top carefully, document any scratches.", isPinned: false } }),
  1632. prisma.note.create({ data: { vehicleId: vehicles[14].id, title: "Manual transmission", content: "David is the primary driver. Wife drives occasionally but doesn't like manual - clutch wear expected.", isPinned: false } }),
  1633. prisma.note.create({ data: { vehicleId: vehicles[15].id, title: "Weekend vehicle", content: "Robert uses this mainly weekends for towing his boat. Look at trailer brakes in spring.", isPinned: false } }),
  1634. prisma.note.create({ data: { vehicleId: vehicles[16].id, title: "TRD package notes", content: "TRD Off-Road suspension - use correct alignment specs (different from base Tacoma).", isPinned: true } }),
  1635. prisma.note.create({ data: { vehicleId: vehicles[19].id, title: "Dump body maintenance", content: "Body floor develops cracks at 150k+ mi. Inspect weld seams during every service.", isPinned: false } }),
  1636. prisma.note.create({ data: { vehicleId: vehicles[20].id, title: "Concrete mixer drum", content: "Drum drive motor is a known weak point on this unit. Monitor for sluggish rotation.", isPinned: true } }),
  1637. prisma.note.create({ data: { vehicleId: vehicles[24].id, title: "Seasonal use", content: "Massey Ferguson is the backup tractor for Prairie Farms. Heavy use spring/fall only.", isPinned: false } }),
  1638. prisma.note.create({ data: { vehicleId: vehicles[25].id, title: "Warranty active", content: "John Deere 8R 410 still under warranty until Jan 2027. Contact JD dealer for anything covered.", isPinned: true } }),
  1639. prisma.note.create({ data: { vehicleId: vehicles[27].id, title: "Harvest critical", content: "S780 combine is CRITICAL for fall harvest. Any work needs to be done before September.", isPinned: true } }),
  1640. prisma.note.create({ data: { vehicleId: vehicles[30].id, title: "D8T parts lead time", content: "CAT D8T parts often take 2-3 weeks. Order ahead. Titan Building will wait if told up front.", isPinned: false } }),
  1641. prisma.note.create({ data: { vehicleId: vehicles[32].id, title: "Undercarriage wear", content: "Track chains due for replacement around 2000hr. Currently at 1800hr — budget planning.", isPinned: false } }),
  1642. prisma.note.create({ data: { vehicleId: vehicles[35].id, title: "Metro City asset tag", content: "City asset tag #WL-014. Include in all invoices for their records.", isPinned: false } }),
  1643. prisma.note.create({ data: { vehicleId: vehicles[37].id, title: "Indoor use only", content: "Electric forklift used indoors at Ironclad warehouse. Tire compound is non-marking.", isPinned: false } }),
  1644. prisma.note.create({ data: { vehicleId: vehicles[38].id, title: "Track car", content: "Sarah takes this to HPDE track days 3-4 times a year. Always check brake pads and fluid before/after.", isPinned: true } }),
  1645. prisma.note.create({ data: { vehicleId: vehicles[38].id, title: "Dry sump oil system", content: "C8 Corvette has a dry sump system. Takes 8 quarts of 0W-40. Use GM-approved Mobil 1 only.", isPinned: false } }),
  1646. prisma.note.create({ data: { vehicleId: vehicles[39].id, title: "Heavy off-road use", content: "Mike takes this on trails every weekend. Always inspect skid plates, check for rock damage underneath.", isPinned: true } }),
  1647. prisma.note.create({ data: { vehicleId: vehicles[40].id, title: "Mopar parts preference", content: "Robert wants Mopar or performance-equivalent parts. No economy parts on this car.", isPinned: true } }),
  1648. prisma.note.create({ data: { vehicleId: vehicles[41].id, title: "Modified intake", content: "Customer installed aftermarket cold air intake. Note: may trigger P0171 lean code - not a fault. Recheck with stock intake if persistent.", isPinned: true } }),
  1649. prisma.note.create({ data: { vehicleId: vehicles[41].id, title: "Catback exhaust", content: "Invidia Q300 catback exhaust installed by customer. Stock exhaust stored at his garage.", isPinned: false } }),
  1650. ]);
  1651. console.log(` Created ${additionalNotes.length} additional vehicle notes`);
  1652. // -- Additional reminders --
  1653. console.log("\nCreating additional reminders...");
  1654. const additionalReminders = await Promise.all([
  1655. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[1].id, title: "Timing belt replacement", description: "Interval-based - EcoBoost timing chain inspection due", dueMileage: 60000 } }),
  1656. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[2].id, title: "Annual inspection", description: "Minnesota state safety inspection", dueDate: days(67) } }),
  1657. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[5].id, title: "Brake fluid flush", description: "Honda recommends every 3 years", dueMileage: 72000, dueDate: days(84) } }),
  1658. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[8].id, title: "Registration renewal", description: "Texas registration expires June 2026", dueDate: days(113) } }),
  1659. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[9].id, title: "Next oil change", description: "Sprinter service B interval", dueMileage: 40000 } }),
  1660. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[10].id, title: "Haldex service", description: "Quattro rear diff fluid change - 40K interval", dueMileage: 40000, isCompleted: true } }),
  1661. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[11].id, title: "Front diff fluid", description: "After lift kit, recommend fluid change at 35K", dueMileage: 35000 } }),
  1662. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[12].id, title: "Annual inspection", description: "Georgia annual safety inspection", dueDate: days(166) } }),
  1663. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[14].id, title: "DSG service interval", description: "Next DSG fluid/filter at 78K", dueMileage: 78000 } }),
  1664. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[17].id, title: "CVT fluid check", description: "Subaru CVT recommended drain/fill", dueMileage: 30000 } }),
  1665. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[20].id, title: "Mixer drum inspection", description: "Annual drum wear/bolt check", dueDate: days(114) } }),
  1666. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[25].id, title: "250hr service", description: "John Deere 8R scheduled interval", dueMileage: 1000 } }),
  1667. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[28].id, title: "Pre-harvest prep", description: "X9 1100 combine full inspection before harvest", dueDate: days(159) } }),
  1668. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[30].id, title: "Blade edge replacement", description: "Check wear on cutting edge", dueMileage: 6500, isCompleted: true } }),
  1669. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[32].id, title: "Undercarriage inspection", description: "2000hr interval for track chain inspection", dueMileage: 2000 } }),
  1670. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[36].id, title: "OSHA crane certification", description: "Annual load test and certification due", dueDate: days(53) } }),
  1671. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[38].id, title: "Corvette annual service", description: "GM recommended annual service with performance inspection", dueDate: days(319) } }),
  1672. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[39].id, title: "Transfer case fluid", description: "Land Rover recommends 40K interval for transfer case fluid", dueMileage: 40000 } }),
  1673. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[40].id, title: "Transmission fluid change", description: "8-speed auto - ZF recommends fluid change at 50K", dueMileage: 50000 } }),
  1674. prisma.reminder.create({ data: { organizationId: ORG_ID, vehicleId: vehicles[41].id, title: "Timing belt inspection", description: "FA24 engine - inspect timing chain tensioner at 60K", dueMileage: 60000 } }),
  1675. ]);
  1676. console.log(` Created ${additionalReminders.length} additional reminders`);
  1677. // -- Vehicle Findings (observations from service) --
  1678. console.log("\nCreating vehicle findings...");
  1679. const findings = await Promise.all([
  1680. prisma.vehicleFinding.create({ data: { vehicleId: vehicles[4].id, description: "Front brake pads at 3mm, recommend replacement within 5,000 miles", severity: "needs_work", status: "open", serviceRecordId: serviceRecords[1].id, notes: "Customer informed during 50K service. Quoted separately.", imageUrls: [] } }),
  1681. prisma.vehicleFinding.create({ data: { vehicleId: vehicles[5].id, description: "CV boot torn on driver side, grease leaking onto suspension components", severity: "urgent", status: "open", notes: "Needs attention soon - will fail CV joint if not addressed.", imageUrls: [] } }),
  1682. prisma.vehicleFinding.create({ data: { vehicleId: vehicles[1].id, description: "Slight oil seepage from valve cover gasket, monitor at next service", severity: "monitor", status: "open", serviceRecordId: serviceRecords[18].id, notes: "Not dripping yet - just damp. Re-check in 6 months.", imageUrls: [] } }),
  1683. prisma.vehicleFinding.create({ data: { vehicleId: vehicles[8].id, description: "Battery testing at 78% capacity, recommend replacement before winter", severity: "monitor", status: "open", notes: "Still starts reliably. Load test shows degraded CCA.", imageUrls: [] } }),
  1684. prisma.vehicleFinding.create({ data: { vehicleId: vehicles[2].id, description: "Tire tread below 4/32 on front tires, will need replacement soon", severity: "needs_work", status: "open", notes: "Rears still have 6/32. Recommend rotating and planning fronts soon.", imageUrls: [] } }),
  1685. prisma.vehicleFinding.create({ data: { vehicleId: vehicles[9].id, description: "Coolant appears contaminated with oil traces, consider flush and inspection", severity: "needs_work", status: "open", serviceRecordId: serviceRecords[13].id, notes: "Found during turbo job. May be related to oil cooler seal.", imageUrls: [] } }),
  1686. prisma.vehicleFinding.create({ data: { vehicleId: vehicles[11].id, description: "Death wobble symptoms reported - track bar bushings worn", severity: "urgent", status: "resolved", notes: "Customer reported highway death wobble. Replaced steering stabilizer and tie rods.", imageUrls: [] } }),
  1687. prisma.vehicleFinding.create({ data: { vehicleId: vehicles[13].id, description: "Rear tire wear uneven - alignment suspected after track use", severity: "needs_work", status: "open", serviceRecordId: serviceRecords[3].id, notes: "Right rear shows inner edge wear consistent with toe-out.", imageUrls: [] } }),
  1688. prisma.vehicleFinding.create({ data: { vehicleId: vehicles[17].id, description: "DEF tank heater intermittent - low DEF temperature warning in cold weather", severity: "monitor", status: "open", serviceRecordId: serviceRecords[4].id, notes: "Only acts up below 20F. Monitor and replace if it fails completely.", imageUrls: [] } }),
  1689. prisma.vehicleFinding.create({ data: { vehicleId: vehicles[18].id, description: "Charge air cooler pipe cracked, boost leak confirmed", severity: "urgent", status: "open", notes: "Part ordered. Causing low-power complaint from driver.", imageUrls: [] } }),
  1690. prisma.vehicleFinding.create({ data: { vehicleId: vehicles[19].id, description: "Dump body floor showing stress cracks near rear hinge", severity: "needs_work", status: "open", notes: "Will need weld repair within 10,000 miles to avoid structural failure.", imageUrls: [] } }),
  1691. prisma.vehicleFinding.create({ data: { vehicleId: vehicles[22].id, description: "Front loader pins showing excessive play, bushings worn", severity: "needs_work", status: "open", notes: "Noticeable play when boom is fully extended. Replace at next service.", imageUrls: [] } }),
  1692. prisma.vehicleFinding.create({ data: { vehicleId: vehicles[23].id, description: "A/C condenser fins damaged, reduced cooling efficiency", severity: "monitor", status: "open", notes: "Fins bent from field debris. Combing/straightening recommended.", imageUrls: [] } }),
  1693. prisma.vehicleFinding.create({ data: { vehicleId: vehicles[29].id, description: "Left blade cylinder showing slow seepage at rod seal", severity: "monitor", status: "resolved", serviceRecordId: serviceRecords[8].id, resolvedServiceRecordId: serviceRecords[8].id, notes: "Resealed during Sept 2025 hydraulic overhaul.", imageUrls: [] } }),
  1694. prisma.vehicleFinding.create({ data: { vehicleId: vehicles[31].id, description: "Boom cylinder showing 2 inch drift over 10 minutes under load", severity: "needs_work", status: "open", notes: "Cylinder reseal scheduled in unassigned work.", imageUrls: [] } }),
  1695. prisma.vehicleFinding.create({ data: { vehicleId: vehicles[32].id, description: "Track shoe bolts loosening, retorque recommended", severity: "monitor", status: "open", notes: "Checked 8 of 42 shoes - 3 required retorque. Full inspection next service.", imageUrls: [] } }),
  1696. prisma.vehicleFinding.create({ data: { vehicleId: vehicles[36].id, description: "Right rear outrigger extending slower than left", severity: "needs_work", status: "open", notes: "Likely valve or cylinder seal. Scheduled for inspection.", imageUrls: [] } }),
  1697. prisma.vehicleFinding.create({ data: { vehicleId: vehicles[24].id, description: "Overheating under heavy PTO load - radiator partially blocked with chaff", severity: "needs_work", status: "open", notes: "Cleaned out what we could. Recommend pull radiator for deep clean.", imageUrls: [] } }),
  1698. prisma.vehicleFinding.create({ data: { vehicleId: vehicles[34].id, description: "Hydraulic hose chafing against frame rail near pivot point", severity: "needs_work", status: "open", notes: "Protective sleeve worn through. Will fail if not rerouted.", imageUrls: [] } }),
  1699. prisma.vehicleFinding.create({ data: { vehicleId: vehicles[16].id, description: "Leaf spring bushings dry and cracking, suspension noise reported", severity: "needs_work", status: "open", notes: "Customer scheduled for leaf spring replacement.", imageUrls: [] } }),
  1700. prisma.vehicleFinding.create({ data: { vehicleId: vehicles[38].id, description: "Rear brake pads showing uneven wear from track use, inner pad thinner than outer", severity: "needs_work", status: "open", notes: "Likely caliper slide pins need cleaning and relubing. Recommend high-performance pads for track use.", imageUrls: [] } }),
  1701. prisma.vehicleFinding.create({ data: { vehicleId: vehicles[39].id, description: "Front left CV boot starting to crack, no grease leaking yet", severity: "monitor", status: "open", notes: "Off-road flex likely contributing. Replace before it tears and contaminates the joint.", imageUrls: [] } }),
  1702. prisma.vehicleFinding.create({ data: { vehicleId: vehicles[40].id, description: "Rear tires wearing faster on inside edge, alignment needed", severity: "needs_work", status: "open", notes: "Aggressive driving habit. Rear camber may be out of spec after pothole.", imageUrls: [] } }),
  1703. prisma.vehicleFinding.create({ data: { vehicleId: vehicles[41].id, description: "Exhaust manifold stud #3 showing early surface corrosion", severity: "monitor", status: "open", notes: "Not leaking yet. Anti-seize on next service. Watch for exhaust tick on cold start.", imageUrls: [] } }),
  1704. ]);
  1705. console.log(` Created ${findings.length} vehicle findings`);
  1706. // -- Repeat service history for predicted maintenance --
  1707. // Pick 6 vehicles, give each 3-4 completed service records with increasing mileage
  1708. // spanning 2-3 years so the predictedMaintenanceActions has regression data.
  1709. console.log("\nCreating repeat service history for predicted maintenance...");
  1710. type HistEntry = { offsetDays: number; mileage: number; title: string; desc: string; type: string; techName: string };
  1711. const predMaintHistory: { vehicleId: string; entries: HistEntry[] }[] = [
  1712. {
  1713. vehicleId: vehicles[1].id, // Ford F-150 Lariat white (Summit Construction, current 42300)
  1714. entries: [
  1715. { offsetDays: -820, mileage: 10500, title: "F-150 - 10K Oil Change", desc: "First service. Synthetic 5W-30, oil filter, tire rotation.", type: "maintenance", techName: "Jake Wilson" },
  1716. { offsetDays: -540, mileage: 20200, title: "F-150 - 20K Service", desc: "Oil change, cabin filter, brake inspection, tire rotation.", type: "maintenance", techName: "Jake Wilson" },
  1717. { offsetDays: -280, mileage: 30100, title: "F-150 - 30K Major Service", desc: "Full fluid service, spark plugs, transfer case fluid.", type: "maintenance", techName: "Chris Taylor" },
  1718. { offsetDays: -90, mileage: 40800, title: "F-150 - 40K Service", desc: "Oil change, brake fluid flush, differential service.", type: "maintenance", techName: "Jake Wilson" },
  1719. ],
  1720. },
  1721. {
  1722. vehicleId: vehicles[4].id, // BMW 330i xDrive (Sarah Coleman, current 55200)
  1723. entries: [
  1724. { offsetDays: -900, mileage: 22000, title: "BMW 330i - 22K Oil Service", desc: "OEM oil, filter, inspection. OEM parts only per customer.", type: "maintenance", techName: "Chris Taylor" },
  1725. { offsetDays: -600, mileage: 32500, title: "BMW 330i - 32K Service", desc: "Oil, cabin filter, brake inspection. All OEM BMW parts.", type: "maintenance", techName: "Chris Taylor" },
  1726. { offsetDays: -300, mileage: 43800, title: "BMW 330i - 43K Service", desc: "Oil change, tire rotation, 4-wheel alignment.", type: "maintenance", techName: "Chris Taylor" },
  1727. ],
  1728. },
  1729. {
  1730. vehicleId: vehicles[5].id, // Honda Civic Sport (Amanda Foster, current 67800)
  1731. entries: [
  1732. { offsetDays: -960, mileage: 28500, title: "Civic - 28K Oil Change", desc: "Synthetic oil, filter, tire rotation.", type: "maintenance", techName: "Jake Wilson" },
  1733. { offsetDays: -680, mileage: 39200, title: "Civic - 39K Service", desc: "Oil change, cabin filter, engine air filter.", type: "maintenance", techName: "Jake Wilson" },
  1734. { offsetDays: -360, mileage: 52000, title: "Civic - 52K Major Service", desc: "Oil, coolant flush, transmission fluid, belt inspection.", type: "maintenance", techName: "Sofia Andersen" },
  1735. { offsetDays: -110, mileage: 64500, title: "Civic - 64K Service", desc: "Oil change, brake pad inspection, alignment check.", type: "maintenance", techName: "Jake Wilson" },
  1736. ],
  1737. },
  1738. {
  1739. vehicleId: vehicles[8].id, // Silverado 2500HD (Lisa Martinez, current 98400)
  1740. entries: [
  1741. { offsetDays: -850, mileage: 62000, title: "Silverado - 62K Oil Service", desc: "Duramax diesel oil, fuel filters, DEF top-off.", type: "maintenance", techName: "Marcus Reed" },
  1742. { offsetDays: -560, mileage: 75500, title: "Silverado - 75K Service", desc: "Oil, fuel filters, DEF fluid, tire rotation.", type: "maintenance", techName: "Marcus Reed" },
  1743. { offsetDays: -290, mileage: 87200, title: "Silverado - 87K Major Service", desc: "Oil, coolant flush, transmission service, DPF regen.", type: "maintenance", techName: "Marcus Reed" },
  1744. { offsetDays: -60, mileage: 96100, title: "Silverado - 96K Service", desc: "Oil change, fuel filter, brake inspection.", type: "maintenance", techName: "Jake Wilson" },
  1745. ],
  1746. },
  1747. {
  1748. vehicleId: vehicles[17].id, // Kenworth T680 (Pacific Freight, current 185000)
  1749. entries: [
  1750. { offsetDays: -780, mileage: 92000, title: "Kenworth - 92K PM", desc: "Full PM - oil, fuel filters, air filters, inspection.", type: "maintenance", techName: "Marcus Reed" },
  1751. { offsetDays: -520, mileage: 118000, title: "Kenworth - 118K PM", desc: "Engine oil, all filters, DEF, grease service.", type: "maintenance", techName: "Marcus Reed" },
  1752. { offsetDays: -260, mileage: 148000, title: "Kenworth - 148K PM", desc: "Engine oil, all filters, coolant check, brake adjust.", type: "maintenance", techName: "Marcus Reed" },
  1753. ],
  1754. },
  1755. {
  1756. vehicleId: vehicles[16].id, // Tacoma TRD Off-Road (Lisa Martinez, current 31200)
  1757. entries: [
  1758. { offsetDays: -790, mileage: 8500, title: "Tacoma - 8K Oil Change", desc: "First service. Synthetic oil, filter, tire rotation.", type: "maintenance", techName: "Jake Wilson" },
  1759. { offsetDays: -520, mileage: 15800, title: "Tacoma - 15K Service", desc: "Oil change, cabin filter, brake inspection.", type: "maintenance", techName: "Jake Wilson" },
  1760. { offsetDays: -250, mileage: 24200, title: "Tacoma - 24K Service", desc: "Oil, tire rotation, differential fluid check.", type: "maintenance", techName: "Erik Haugen" },
  1761. ],
  1762. },
  1763. ];
  1764. const today = TODAY;
  1765. const predMaintRecords = [];
  1766. for (const hist of predMaintHistory) {
  1767. for (const entry of hist.entries) {
  1768. const serviceDate = new Date(today);
  1769. serviceDate.setDate(serviceDate.getDate() + entry.offsetDays);
  1770. const rec = await prisma.serviceRecord.create({
  1771. data: {
  1772. organizationId: ORG_ID,
  1773. vehicleId: hist.vehicleId,
  1774. title: entry.title,
  1775. description: entry.desc,
  1776. type: entry.type,
  1777. status: "completed",
  1778. serviceDate,
  1779. mileage: entry.mileage,
  1780. techName: entry.techName,
  1781. shopName: "Egeland Auto",
  1782. subtotal: 180,
  1783. taxRate: 8,
  1784. taxAmount: 14.40,
  1785. totalAmount: 194.40,
  1786. cost: 194.40,
  1787. partItems: { create: [{ name: "Synthetic Oil (5qt)", partNumber: "OIL-SYN-5Q", quantity: 1, unitPrice: 45, total: 45 }, { name: "Oil Filter", partNumber: "OF-UNI", quantity: 1, unitPrice: 12, total: 12 }] },
  1788. laborItems: { create: [{ description: "Service and inspection", hours: 1.2, rate: 102.50, total: 123 }] },
  1789. },
  1790. });
  1791. predMaintRecords.push(rec);
  1792. }
  1793. }
  1794. console.log(` Created ${predMaintRecords.length} historical service records across ${predMaintHistory.length} vehicles`);
  1795. // Enable predicted maintenance feature setting
  1796. console.log("\nEnabling predicted maintenance feature...");
  1797. await Promise.all([
  1798. prisma.appSetting.upsert({
  1799. where: { organizationId_key: { organizationId: ORG_ID, key: "maintenance.enabled" } },
  1800. create: { organizationId: ORG_ID, key: "maintenance.enabled", value: "true", userId: USER_ID },
  1801. update: { value: "true" },
  1802. }),
  1803. prisma.appSetting.upsert({
  1804. where: { organizationId_key: { organizationId: ORG_ID, key: "maintenance.serviceInterval" } },
  1805. create: { organizationId: ORG_ID, key: "maintenance.serviceInterval", value: "15000", userId: USER_ID },
  1806. update: { value: "15000" },
  1807. }),
  1808. prisma.appSetting.upsert({
  1809. where: { organizationId_key: { organizationId: ORG_ID, key: "maintenance.approachingThreshold" } },
  1810. create: { organizationId: ORG_ID, key: "maintenance.approachingThreshold", value: "1000", userId: USER_ID },
  1811. update: { value: "1000" },
  1812. }),
  1813. ]);
  1814. console.log(" Enabled predicted maintenance (interval: 15,000 mi, approaching: 1,000 mi)");
  1815. // -- SMS Messages --
  1816. console.log("\nCreating SMS messages...");
  1817. const shopNumber = "+15555550100";
  1818. await Promise.all([
  1819. prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550101", body: "Hi Summit Construction, your Ford F-150 (CO-4455) transmission flush is complete. Total: $689.04. Ready for pickup.", status: "delivered", organizationId: ORG_ID, customerId: customers[0].id } }),
  1820. prisma.smsMessage.create({ data: { direction: "inbound", fromNumber: "+15555550101", toNumber: shopNumber, body: "Thanks! We'll send someone before 4pm. Can you look at the D6 bulldozer next week? Blade cylinder is leaking again.", status: "received", organizationId: ORG_ID, customerId: customers[0].id, createdAt: new Date(Date.now() - 3500000) } }),
  1821. prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550101", body: "Absolutely, we can fit the D6 in on Tuesday. We'll order the seal kit ahead of time.", status: "delivered", organizationId: ORG_ID, customerId: customers[0].id, createdAt: new Date(Date.now() - 3400000) } }),
  1822. prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550102", body: "Hi James, your Toyota Camry is due for its 30K service. We have Thursday or Friday availability.", status: "delivered", organizationId: ORG_ID, customerId: customers[1].id, createdAt: new Date(Date.now() - 86400000) } }),
  1823. prisma.smsMessage.create({ data: { direction: "inbound", fromNumber: "+15555550102", toNumber: shopNumber, body: "Thursday morning works. Synthetic oil like last time please.", status: "received", organizationId: ORG_ID, customerId: customers[1].id, createdAt: new Date(Date.now() - 82800000) } }),
  1824. prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550102", body: "Thursday 8am confirmed. Full synthetic 0W-20 as always. See you then!", status: "delivered", organizationId: ORG_ID, customerId: customers[1].id, createdAt: new Date(Date.now() - 82000000) } }),
  1825. prisma.smsMessage.create({ data: { direction: "inbound", fromNumber: "+15555550103", toNumber: shopNumber, body: "Our Volvo FH 640 (unit #5) is showing a check engine light. Can you get it in ASAP?", status: "received", organizationId: ORG_ID, customerId: customers[2].id, createdAt: new Date(Date.now() - 172800000) } }),
  1826. prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550103", body: "We can take the Volvo FH 640 first thing tomorrow. Have the driver bring it in by 7am.", status: "delivered", organizationId: ORG_ID, customerId: customers[2].id, createdAt: new Date(Date.now() - 172000000) } }),
  1827. prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550103", body: "Update on the Volvo FH 640: DOT inspection in progress. Minor issues with marker lights and wipers. Done by 3pm.", status: "delivered", organizationId: ORG_ID, customerId: customers[2].id, createdAt: new Date(Date.now() - 86400000) } }),
  1828. prisma.smsMessage.create({ data: { direction: "inbound", fromNumber: "+15555550111", toNumber: shopNumber, body: "Our John Deere 6R is running rough and hydraulics feel sluggish. Planting season starts in 3 weeks!", status: "received", organizationId: ORG_ID, customerId: customers[10].id, createdAt: new Date(Date.now() - 432000000) } }),
  1829. prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550111", body: "Bring the John Deere 6R in this week, we'll bump it to priority. Likely needs 500hr service + hydraulic analysis.", status: "delivered", organizationId: ORG_ID, customerId: customers[10].id, createdAt: new Date(Date.now() - 428400000) } }),
  1830. prisma.smsMessage.create({ data: { direction: "inbound", fromNumber: "+15555550111", toNumber: shopNumber, body: "Great. The Massey Ferguson 8S has low hydraulic pressure too. Can you look at both?", status: "received", organizationId: ORG_ID, customerId: customers[10].id, createdAt: new Date(Date.now() - 424800000) } }),
  1831. prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550111", body: "Bring both Wednesday. JD done same day, Massey Thursday. Both ready before planting.", status: "delivered", organizationId: ORG_ID, customerId: customers[10].id, createdAt: new Date(Date.now() - 421200000) } }),
  1832. prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550116", body: "Kevin, your Sprinter turbo has excessive shaft play causing black smoke. Replacement: $1,280 + labor. Proceed?", status: "delivered", organizationId: ORG_ID, customerId: customers[15].id, createdAt: new Date(Date.now() - 345600000) } }),
  1833. prisma.smsMessage.create({ data: { direction: "inbound", fromNumber: "+15555550116", toNumber: shopNumber, body: "Ouch. Yeah go ahead, I need the van for catering events next week.", status: "received", organizationId: ORG_ID, customerId: customers[15].id, createdAt: new Date(Date.now() - 342000000) } }),
  1834. prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550116", body: "Parts arrive Thursday, done Friday afternoon. Total: $2,243.70 including oil change.", status: "delivered", organizationId: ORG_ID, customerId: customers[15].id, createdAt: new Date(Date.now() - 338400000) } }),
  1835. prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550109", body: "Ridgeline, the Komatsu PC210 track chains are done. Both sides + new sprockets. Total: $9,309.60. Ready for transport.", status: "delivered", organizationId: ORG_ID, customerId: customers[8].id, createdAt: new Date(Date.now() - 604800000) } }),
  1836. prisma.smsMessage.create({ data: { direction: "inbound", fromNumber: "+15555550109", toNumber: shopNumber, body: "Excellent work. Sending the lowboy tomorrow. Also sending the Volvo EC220E for 1000hr service.", status: "received", organizationId: ORG_ID, customerId: customers[8].id, createdAt: new Date(Date.now() - 601200000) } }),
  1837. prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550109", body: "We have Volvo EC220E filters in stock. 1-2 day turnaround once it arrives.", status: "delivered", organizationId: ORG_ID, customerId: customers[8].id, createdAt: new Date(Date.now() - 597600000) } }),
  1838. prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550104", body: "Hi Sarah, your Porsche 911 annual service is complete. Total: $832.14. All OEM parts as always.", status: "delivered", organizationId: ORG_ID, customerId: customers[3].id, createdAt: new Date(Date.now() - 259200000) } }),
  1839. prisma.smsMessage.create({ data: { direction: "inbound", fromNumber: "+15555550104", toNumber: shopNumber, body: "Perfect, thank you. I also want to get a quote on the BMW brake job with OEM rotors.", status: "received", organizationId: ORG_ID, customerId: customers[3].id, createdAt: new Date(Date.now() - 255600000) } }),
  1840. prisma.smsMessage.create({ data: { direction: "outbound", fromNumber: shopNumber, toNumber: "+15555550104", body: "Quote sent to your email - $1,711.80 for the full brake overhaul with OEM rotors and pads. Valid until March 15.", status: "delivered", organizationId: ORG_ID, customerId: customers[3].id, createdAt: new Date(Date.now() - 254000000) } }),
  1841. ]);
  1842. console.log(" Created 22 SMS messages");
  1843. // -- Technicians & Work Board --
  1844. console.log("\nCreating technicians...");
  1845. // Clean up existing workboard data
  1846. await prisma.technician.deleteMany({ where: { organizationId: ORG_ID } });
  1847. const technicians = await Promise.all([
  1848. prisma.technician.create({ data: { name: "Jake Wilson", color: "#3b82f6", sortOrder: 0, organizationId: ORG_ID } }),
  1849. prisma.technician.create({ data: { name: "Chris Taylor", color: "#22c55e", sortOrder: 1, organizationId: ORG_ID } }),
  1850. prisma.technician.create({ data: { name: "Marcus Reed", color: "#f59e0b", sortOrder: 2, organizationId: ORG_ID } }),
  1851. prisma.technician.create({ data: { name: "Sofia Andersen", color: "#ec4899", sortOrder: 3, organizationId: ORG_ID } }),
  1852. prisma.technician.create({ data: { name: "Erik Haugen", color: "#8b5cf6", sortOrder: 4, organizationId: ORG_ID } }),
  1853. prisma.technician.create({ data: { name: "Lars Johansen", color: "#06b6d4", sortOrder: 5, organizationId: ORG_ID } }),
  1854. prisma.technician.create({ data: { name: "Nina Berglund", color: "#f97316", sortOrder: 6, organizationId: ORG_ID } }),
  1855. prisma.technician.create({ data: { name: "Tom Bradley", color: "#14b8a6", sortOrder: 7, organizationId: ORG_ID } }),
  1856. prisma.technician.create({ data: { name: "Kari Moen", color: "#a855f7", sortOrder: 8, organizationId: ORG_ID } }),
  1857. prisma.technician.create({ data: { name: "Daniel Eriksen", color: "#ef4444", sortOrder: 9, organizationId: ORG_ID } }),
  1858. // Demo Owner (index 10) — linked to the demo user so dashboard "My Active Jobs" populates
  1859. prisma.technician.create({ data: { name: "Demo Owner", color: "#0ea5e9", sortOrder: 10, organizationId: ORG_ID, userId: USER_ID } }),
  1860. ]);
  1861. console.log(` Created ${technicians.length} technicians`);
  1862. // Board assignments - spread across the current week
  1863. console.log("\nCreating board assignments...");
  1864. // Monday of the week the board showcases - see weekMonday().
  1865. const monday = weekMonday();
  1866. const day = (offset: number) => {
  1867. const d = new Date(monday);
  1868. d.setDate(d.getDate() + offset);
  1869. return d;
  1870. };
  1871. // Helper: create a datetime on a given day offset at a specific hour:minute
  1872. const dt = (dayOffset: number, hour: number, minute = 0) => {
  1873. const d = new Date(monday);
  1874. d.setDate(d.getDate() + dayOffset);
  1875. d.setHours(hour, minute, 0, 0);
  1876. return d;
  1877. };
  1878. // Helper to create service record with parts + labor
  1879. // Typed against the Prisma input, not `Record<string, unknown>` cast to
  1880. // never: that cast is what let 29 jobs go in as "scheduled" unnoticed.
  1881. type JobBase = Omit<
  1882. Prisma.ServiceRecordUncheckedCreateInput,
  1883. "organizationId" | "status" | "subtotal" | "taxRate" | "taxAmount" | "totalAmount" | "cost" | "diagnosticNotes" | "partItems" | "laborItems"
  1884. > & { status: JobStatus };
  1885. const sr = (base: JobBase, parts: { name: string; partNumber: string; quantity: number; unit?: string; unitPrice: number; total: number }[], labor: { description: string; hours: number; rate: number; total: number }[], notes?: string) => {
  1886. const subtotal = parts.reduce((s, p) => s + p.total, 0) + labor.reduce((s, l) => s + l.total, 0);
  1887. const taxAmount = Math.round(subtotal * 0.08 * 100) / 100;
  1888. return prisma.serviceRecord.create({ data: { ...base, organizationId: ORG_ID, subtotal, taxRate: 8, taxAmount, totalAmount: subtotal + taxAmount, cost: subtotal + taxAmount, diagnosticNotes: notes || null, partItems: { create: parts }, laborItems: { create: labor } } });
  1889. };
  1890. // Create service records for the board (assigned ones)
  1891. const boardServiceRecords = await Promise.all([
  1892. // 0: Honda Civic AC
  1893. sr({ vehicleId: vehicles[5].id, title: "Honda Civic - AC Compressor", description: "AC not blowing cold. Compressor clutch not engaging.", type: "repair", status: "pending", serviceDate: day(0), mileage: 45200, techName: "Jake Wilson", shopName: "Egeland Auto" },
  1894. [{ name: "AC Compressor Assembly", partNumber: "HON-ACC-10G", quantity: 1, unitPrice: 480, total: 480 }, { name: "Refrigerant R-1234yf (bulk)", partNumber: "R1234YF-BULK", quantity: 1.4, unit: "lb", unitPrice: 68, total: 95.20 }],
  1895. [{ description: "AC compressor removal and replacement", hours: 2.5, rate: 110, total: 275 }, { description: "System evacuate, recharge and leak test", hours: 1.0, rate: 110, total: 110 }],
  1896. "Compressor clutch not engaging. Checked voltage at clutch connector - 12V present. Clutch coil resistance out of spec. Replace compressor assembly."),
  1897. // 1: Audi A4 timing belt
  1898. sr({ vehicleId: vehicles[10].id, title: "Audi A4 - Timing Belt Service", description: "Scheduled timing belt and water pump replacement at 60K miles.", type: "maintenance", status: "in-progress", serviceDate: day(0), mileage: 60000, techName: "Chris Taylor", shopName: "Egeland Auto" },
  1899. [{ name: "Timing Belt Kit w/ Water Pump", partNumber: "AUD-TBK-A4", quantity: 1, unitPrice: 385, total: 385 }, { name: "Thermostat Assembly", partNumber: "AUD-TH-A4", quantity: 1, unitPrice: 65, total: 65 }, { name: "Coolant G13 (5L)", partNumber: "AUD-COOL-G13", quantity: 1, unitPrice: 42, total: 42 }],
  1900. [{ description: "Timing belt and water pump replacement", hours: 4.5, rate: 120, total: 540 }, { description: "Coolant flush and bleed", hours: 1.0, rate: 120, total: 120 }]),
  1901. // 2: Tesla Model Y suspension
  1902. sr({ vehicleId: vehicles[7].id, title: "Tesla Model Y - Suspension Noise", description: "Customer reports clunking from front left over bumps.", type: "repair", status: "pending", serviceDate: day(1), mileage: 32000, techName: "Sofia Andersen", shopName: "Egeland Auto" },
  1903. [{ name: "Front Lower Control Arm (L)", partNumber: "TES-LCA-MY-L", quantity: 1, unitPrice: 290, total: 290 }],
  1904. [{ description: "Diagnose suspension noise", hours: 0.5, rate: 110, total: 55 }, { description: "Control arm replacement and alignment", hours: 2.0, rate: 110, total: 220 }],
  1905. "Play found in front left lower control arm ball joint. No other issues found. Recommend alignment after replacement."),
  1906. // 3: Silverado 60K
  1907. sr({ vehicleId: vehicles[8].id, title: "Silverado - 60K Service", description: "60,000 mile major service. Transmission fluid, spark plugs, coolant flush.", type: "maintenance", status: "pending", serviceDate: day(1), mileage: 60000, techName: "Jake Wilson", shopName: "Egeland Auto" },
  1908. [{ name: "ATF Dexron VI (12qt)", partNumber: "GM-ATF-DEX6", quantity: 1, unitPrice: 120, total: 120 }, { name: "Spark Plugs ACDelco (8)", partNumber: "ACD-SP-53", quantity: 8, unitPrice: 12, total: 96 }, { name: "Coolant Dex-Cool (2gal)", partNumber: "GM-COOL-DC", quantity: 1, unitPrice: 38, total: 38 }],
  1909. [{ description: "Transmission fluid exchange", hours: 1.5, rate: 110, total: 165 }, { description: "Spark plug replacement (V8)", hours: 1.5, rate: 110, total: 165 }, { description: "Coolant flush and refill", hours: 1.0, rate: 110, total: 110 }]),
  1910. // 4: Tacoma leaf springs
  1911. sr({ vehicleId: vehicles[16].id, title: "Toyota Tacoma - Leaf Spring Replacement", description: "Rear leaf springs sagging. Replace with upgraded OEM springs.", type: "repair", status: "pending", serviceDate: day(1), mileage: 55000, techName: "Erik Haugen", shopName: "Egeland Auto" },
  1912. [{ name: "Rear Leaf Spring Pack (pair)", partNumber: "TOY-LS-TAC", quantity: 1, unitPrice: 520, total: 520 }, { name: "U-Bolt Kit", partNumber: "TOY-UB-TAC", quantity: 1, unitPrice: 85, total: 85 }],
  1913. [{ description: "Leaf spring removal and installation (both sides)", hours: 3.5, rate: 110, total: 385 }]),
  1914. // 5: Bobcat S650 annual
  1915. sr({ vehicleId: vehicles[32].id, title: "Bobcat S650 - Annual Service", description: "Full annual service. Engine oil, hydraulic filters, drive belt.", type: "maintenance", status: "pending", serviceDate: day(2), mileage: 2200, techName: "Marcus Reed", shopName: "Egeland Auto" },
  1916. [{ name: "Engine Oil 15W-40 (2gal)", partNumber: "BOB-15W40-2G", quantity: 1, unitPrice: 68, total: 68 }, { name: "Oil Filter", partNumber: "BOB-OF-S650", quantity: 1, unitPrice: 22, total: 22 }, { name: "Hydraulic Filter Set", partNumber: "BOB-HFS-S650", quantity: 1, unitPrice: 95, total: 95 }, { name: "Drive Belt", partNumber: "BOB-DB-S650", quantity: 1, unitPrice: 48, total: 48 }],
  1917. [{ description: "Engine oil and filter change", hours: 1.0, rate: 130, total: 130 }, { description: "Hydraulic filter replacement", hours: 1.0, rate: 130, total: 130 }, { description: "Drive belt inspection and replacement", hours: 0.5, rate: 130, total: 65 }]),
  1918. // 6: CAT 745 transmission rebuild
  1919. sr({ vehicleId: vehicles[34].id, title: "CAT 745 - Transmission Rebuild", description: "Slipping in 3rd gear under load. Full transmission rebuild.", type: "repair", status: "in-progress", serviceDate: day(0), mileage: 8500, techName: "Marcus Reed", shopName: "Egeland Auto" },
  1920. [{ name: "Transmission Rebuild Kit", partNumber: "CAT-TRK-745", quantity: 1, unitPrice: 4200, total: 4200 }, { name: "Torque Converter", partNumber: "CAT-TC-745", quantity: 1, unitPrice: 2800, total: 2800 }, { name: "Transmission Oil (20gal)", partNumber: "CAT-TO-20G", quantity: 1, unitPrice: 480, total: 480 }],
  1921. [{ description: "Transmission removal", hours: 8.0, rate: 150, total: 1200 }, { description: "Rebuild and reassembly", hours: 16.0, rate: 150, total: 2400 }, { description: "Installation and calibration", hours: 6.0, rate: 150, total: 900 }],
  1922. "3rd gear clutch pack worn beyond spec. Torque converter showing excessive slip. Full rebuild recommended over clutch pack only due to hours on unit."),
  1923. // 7: F-150 exhaust
  1924. sr({ vehicleId: vehicles[3].id, title: "Ford F-150 - Exhaust Leak Repair", description: "Exhaust manifold gasket leak. Replace gasket and inspect cat.", type: "repair", status: "pending", serviceDate: day(2), mileage: 52000, techName: "Jake Wilson", shopName: "Egeland Auto" },
  1925. [{ name: "Exhaust Manifold Gasket Set", partNumber: "FORD-EMG-5L", quantity: 1, unitPrice: 85, total: 85 }, { name: "Exhaust Manifold Bolts (set)", partNumber: "FORD-EMB-5L", quantity: 1, unitPrice: 42, total: 42 }],
  1926. [{ description: "Manifold removal and gasket replacement", hours: 2.5, rate: 110, total: 275 }, { description: "Cat converter inspection", hours: 0.5, rate: 110, total: 55 }]),
  1927. // 8: Ram transfer case
  1928. sr({ vehicleId: vehicles[16].id, title: "Ram 1500 - Transfer Case Service", description: "Transfer case fluid change. Customer reports 4WD engagement delay.", type: "maintenance", status: "pending", serviceDate: day(3), mileage: 45000, techName: "Sofia Andersen", shopName: "Egeland Auto" },
  1929. [{ name: "Transfer Case Fluid (2qt)", partNumber: "RAM-TCF-2Q", quantity: 1, unitPrice: 48, total: 48 }, { name: "Transfer Case Drain Plug Gasket", partNumber: "RAM-DPG-TC", quantity: 1, unitPrice: 8, total: 8 }],
  1930. [{ description: "Transfer case fluid drain and refill", hours: 1.0, rate: 110, total: 110 }, { description: "4WD engagement test and inspection", hours: 0.5, rate: 110, total: 55 }],
  1931. "Fluid was dark and slightly burnt. Delay was from low fluid level. Refilled and tested - engages promptly now. Monitor."),
  1932. // 9: Combine pre-season
  1933. sr({ vehicleId: vehicles[27].id, title: "Combine Harvester - Pre-Season Prep", description: "Full pre-season inspection. Header, feeder chain, sieve adjustments, all fluids.", type: "maintenance", status: "pending", serviceDate: day(4), mileage: 2800, techName: "Marcus Reed", shopName: "Egeland Auto" },
  1934. [{ name: "Engine Oil 15W-40 (5gal)", partNumber: "CMB-15W40-5G", quantity: 1, unitPrice: 185, total: 185 }, { name: "Hydraulic Filter Set", partNumber: "CMB-HFS", quantity: 1, unitPrice: 110, total: 110 }, { name: "Feeder Chain", partNumber: "CMB-FC-1", quantity: 1, unitPrice: 380, total: 380 }, { name: "Sieve Section (2)", partNumber: "CMB-SS-2", quantity: 2, unitPrice: 145, total: 290 }],
  1935. [{ description: "Engine oil and filter change", hours: 1.5, rate: 140, total: 210 }, { description: "Feeder chain replacement", hours: 2.5, rate: 140, total: 350 }, { description: "Sieve adjustment and replacement", hours: 2.0, rate: 140, total: 280 }, { description: "Full system inspection and grease", hours: 2.0, rate: 140, total: 280 }]),
  1936. // 10: Jeep axle seal
  1937. sr({ vehicleId: vehicles[12].id, title: "Jeep Wrangler - Axle Seal Replacement", description: "Rear axle seal leaking. Replace seals and check diff fluid.", type: "repair", status: "pending", serviceDate: day(4), mileage: 62000, techName: "Chris Taylor", shopName: "Egeland Auto" },
  1938. [{ name: "Rear Axle Seal Kit", partNumber: "JEEP-ASK-JL", quantity: 1, unitPrice: 65, total: 65 }, { name: "Differential Fluid 75W-140 (2.5qt)", partNumber: "JEEP-DF-75W", quantity: 1, unitPrice: 52, total: 52 }],
  1939. [{ description: "Axle seal replacement (both sides)", hours: 2.5, rate: 110, total: 275 }, { description: "Differential fluid top-off and inspection", hours: 0.5, rate: 110, total: 55 }]),
  1940. // 11: Ram oil change (Lars)
  1941. sr({ vehicleId: vehicles[15].id, title: "Ram 1500 - Oil Change & Inspection", description: "Regular 10K mile service. Check brakes and tires.", type: "maintenance", status: "pending", serviceDate: day(0), mileage: 30000, techName: "Lars Johansen", shopName: "Egeland Auto" },
  1942. [{ name: "Engine Oil 5W-20 (7qt)", partNumber: "RAM-5W20-7Q", quantity: 1, unitPrice: 52, total: 52 }, { name: "Oil Filter", partNumber: "RAM-OF-HEMI", quantity: 1, unitPrice: 14, total: 14 }],
  1943. [{ description: "Oil and filter change", hours: 0.5, rate: 110, total: 55 }, { description: "Brake and tire inspection", hours: 0.5, rate: 110, total: 55 }]),
  1944. // 12: F-150 wheel bearing (Nina)
  1945. sr({ vehicleId: vehicles[2].id, title: "Ford F-150 - Wheel Bearing", description: "Front left wheel bearing noise. Replace bearing assembly.", type: "repair", status: "waiting-parts", serviceDate: day(1), mileage: 68000, techName: "Nina Berglund", shopName: "Egeland Auto" },
  1946. [{ name: "Front Wheel Bearing Hub Assembly", partNumber: "FORD-WBH-F150", quantity: 1, unitPrice: 285, total: 285 }],
  1947. [{ description: "Wheel bearing hub replacement", hours: 2.0, rate: 110, total: 220 }, { description: "Road test and verify", hours: 0.5, rate: 110, total: 55 }],
  1948. "Confirmed bearing noise from front left. Bearing has play when checked on lift. Right side OK for now."),
  1949. // 13: Concrete mixer (Tom)
  1950. sr({ vehicleId: vehicles[20].id, title: "Concrete Mixer - Drum Motor Service", description: "Hydraulic motor service for drum rotation. Sluggish under load.", type: "repair", status: "in-progress", serviceDate: day(0), mileage: 75000, techName: "Tom Bradley", shopName: "Egeland Auto" },
  1951. [{ name: "Hydraulic Motor Seal Kit", partNumber: "MIX-HMSK", quantity: 1, unitPrice: 280, total: 280 }, { name: "Hydraulic Oil ISO 46 (10gal)", partNumber: "HYD-ISO46-10G", quantity: 1, unitPrice: 180, total: 180 }, { name: "Hydraulic Hose Assembly", partNumber: "MIX-HHA-12", quantity: 2, unitPrice: 95, total: 190 }],
  1952. [{ description: "Hydraulic motor removal and reseal", hours: 4.0, rate: 140, total: 560 }, { description: "Hose replacement and system flush", hours: 2.0, rate: 140, total: 280 }, { description: "Load test drum rotation", hours: 1.0, rate: 140, total: 140 }]),
  1953. // 14: Tractor PTO (Kari)
  1954. sr({ vehicleId: vehicles[25].id, title: "Tractor - PTO Clutch Adjustment", description: "PTO slipping under heavy load. Adjust clutch pack and inspect.", type: "repair", status: "pending", serviceDate: day(2), mileage: 3200, techName: "Kari Moen", shopName: "Egeland Auto" },
  1955. [{ name: "PTO Clutch Friction Disc Set", partNumber: "TRC-PTO-FDS", quantity: 1, unitPrice: 165, total: 165 }],
  1956. [{ description: "PTO clutch pack inspection and adjustment", hours: 2.0, rate: 130, total: 260 }],
  1957. "Clutch pack within adjustment range. Adjusted gap to spec. If slipping persists, full clutch pack replacement needed."),
  1958. // 15: Dump truck tailgate (Daniel)
  1959. sr({ vehicleId: vehicles[21].id, title: "Dump Truck - Tailgate Cylinder", description: "Tailgate cylinder leaking. Reseal and test.", type: "repair", status: "pending", serviceDate: day(1), mileage: 120000, techName: "Daniel Eriksen", shopName: "Egeland Auto" },
  1960. [{ name: "Tailgate Cylinder Seal Kit", partNumber: "DMP-TCSK", quantity: 1, unitPrice: 145, total: 145 }, { name: "Hydraulic Oil ISO 46 (5gal)", partNumber: "HYD-ISO46-5G", quantity: 1, unitPrice: 95, total: 95 }],
  1961. [{ description: "Cylinder removal and reseal", hours: 3.0, rate: 130, total: 390 }, { description: "Reinstall and cycle test", hours: 1.0, rate: 130, total: 130 }]),
  1962. // 16: Forklift mast chain (Lars)
  1963. sr({ vehicleId: vehicles[37].id, title: "Forklift - Mast Chain Replace", description: "Mast chain stretched beyond spec. Replace chains and adjust.", type: "repair", status: "pending", serviceDate: day(3), mileage: 5500, techName: "Lars Johansen", shopName: "Egeland Auto" },
  1964. [{ name: "Mast Chain (pair)", partNumber: "FRK-MC-PAIR", quantity: 1, unitPrice: 420, total: 420 }, { name: "Chain Anchor Pins", partNumber: "FRK-CAP-4", quantity: 1, unitPrice: 35, total: 35 }],
  1965. [{ description: "Mast chain removal and replacement", hours: 2.5, rate: 120, total: 300 }, { description: "Chain tension adjustment and mast alignment", hours: 1.0, rate: 120, total: 120 }]),
  1966. // 17: VW Golf clutch (Nina)
  1967. sr({ vehicleId: vehicles[14].id, title: "VW Golf GTI - Clutch Replace", description: "Clutch slipping at high RPM. Replace disc, pressure plate, throwout bearing.", type: "repair", status: "pending", serviceDate: day(2), mileage: 72000, techName: "Nina Berglund", shopName: "Egeland Auto" },
  1968. [{ name: "Clutch Kit (disc, plate, bearing)", partNumber: "VW-CK-GTI", quantity: 1, unitPrice: 480, total: 480 }, { name: "Flywheel Bolts (set)", partNumber: "VW-FWB-GTI", quantity: 1, unitPrice: 32, total: 32 }],
  1969. [{ description: "Transmission removal", hours: 3.5, rate: 120, total: 420 }, { description: "Clutch replacement and installation", hours: 2.5, rate: 120, total: 300 }, { description: "Bleed hydraulic clutch and road test", hours: 1.0, rate: 120, total: 120 }]),
  1970. // 18: Combine header knives (Tom)
  1971. sr({ vehicleId: vehicles[28].id, title: "Combine - Header Knife Sharpen", description: "Sharpen and replace worn header knives. 4 sections need new blades.", type: "maintenance", status: "pending", serviceDate: day(4), mileage: 2900, techName: "Tom Bradley", shopName: "Egeland Auto" },
  1972. [{ name: "Header Knife Sections (pack of 10)", partNumber: "CMB-HKS-10", quantity: 1, unitPrice: 85, total: 85 }, { name: "Knife Rivets (50-pack)", partNumber: "CMB-KR-50", quantity: 1, unitPrice: 22, total: 22 }],
  1973. [{ description: "Remove header knives", hours: 1.0, rate: 120, total: 120 }, { description: "Sharpen and replace worn sections", hours: 2.0, rate: 120, total: 240 }, { description: "Reinstall and test", hours: 1.0, rate: 120, total: 120 }]),
  1974. // 19: Kenworth clutch (Kari)
  1975. sr({ vehicleId: vehicles[17].id, title: "Kenworth T680 - Clutch Adjustment", description: "Driver reports clutch engagement point too high. Adjust and inspect.", type: "repair", status: "pending", serviceDate: day(3), mileage: 185000, techName: "Kari Moen", shopName: "Egeland Auto" },
  1976. [{ name: "Clutch Adjustment Hardware", partNumber: "KW-CAH-T680", quantity: 1, unitPrice: 45, total: 45 }],
  1977. [{ description: "Clutch linkage inspection and adjustment", hours: 1.5, rate: 140, total: 210 }, { description: "Road test under load", hours: 0.5, rate: 140, total: 70 }]),
  1978. // 20: Jeep steering stabilizer (Daniel)
  1979. sr({ vehicleId: vehicles[11].id, title: "Jeep Wrangler - Steering Stabilizer", description: "Death wobble at highway speed. Replace stabilizer and check tie rods.", type: "repair", status: "pending", serviceDate: day(4), mileage: 31000, techName: "Daniel Eriksen", shopName: "Egeland Auto" },
  1980. [{ name: "Steering Stabilizer", partNumber: "JEEP-SS-JL", quantity: 1, unitPrice: 125, total: 125 }, { name: "Tie Rod End (pair)", partNumber: "JEEP-TRE-JL", quantity: 1, unitPrice: 140, total: 140 }],
  1981. [{ description: "Steering stabilizer replacement", hours: 0.5, rate: 110, total: 55 }, { description: "Tie rod end inspection and replacement", hours: 1.5, rate: 110, total: 165 }]),
  1982. // Friday extras (20–29)
  1983. // 21: Camry cabin filter (Jake)
  1984. sr({ vehicleId: vehicles[0].id, title: "Toyota Camry - Cabin Filter + Wipers", description: "Replace cabin air filter and wiper blades. Quick service.", type: "maintenance", status: "pending", serviceDate: day(4), mileage: 16500, techName: "Jake Wilson", shopName: "Egeland Auto" },
  1985. [{ name: "Cabin Air Filter", partNumber: "TOY-CAF-CAM", quantity: 1, unitPrice: 22, total: 22 }, { name: "Wiper Blade Set (front)", partNumber: "TOY-WB-CAM", quantity: 1, unitPrice: 38, total: 38 }],
  1986. [{ description: "Cabin filter and wiper replacement", hours: 0.3, rate: 95, total: 28.50 }]),
  1987. // 22: Honda brakes (Jake)
  1988. sr({ vehicleId: vehicles[5].id, title: "Honda Civic - Brake Pad Replace", description: "Front pads worn to 2mm. Replace front brake pads.", type: "repair", status: "pending", serviceDate: day(4), mileage: 46000, techName: "Jake Wilson", shopName: "Egeland Auto" },
  1989. [{ name: "Front Brake Pad Set", partNumber: "HON-BP-CIV", quantity: 1, unitPrice: 85, total: 85 }, { name: "Brake Cleaner (2 cans)", partNumber: "BC-CLEAN-2", quantity: 1, unitPrice: 12, total: 12 }],
  1990. [{ description: "Front brake pad replacement", hours: 1.0, rate: 95, total: 95 }, { description: "Brake inspection and road test", hours: 0.3, rate: 95, total: 28.50 }]),
  1991. // 23: Silverado taillight (Jake)
  1992. sr({ vehicleId: vehicles[8].id, title: "Silverado - Taillight Assembly", description: "Cracked right taillight from parking lot bump. Replace assembly.", type: "repair", status: "pending", serviceDate: day(4), mileage: 60500, techName: "Jake Wilson", shopName: "Egeland Auto" },
  1993. [{ name: "Taillight Assembly (right)", partNumber: "GM-TLA-SIL-R", quantity: 1, unitPrice: 125, total: 125 }],
  1994. [{ description: "Taillight assembly replacement", hours: 0.5, rate: 95, total: 47.50 }]),
  1995. // 24: Sprinter oil (Chris)
  1996. sr({ vehicleId: vehicles[9].id, title: "Sprinter - Oil Service + Inspection", description: "Regular oil change with full inspection. Check brake wear sensors.", type: "maintenance", status: "pending", serviceDate: day(4), mileage: 37000, techName: "Chris Taylor", shopName: "Egeland Auto" },
  1997. [{ name: "Engine Oil 5W-30 (8qt)", partNumber: "MB-5W30-8Q", quantity: 1, unitPrice: 78, total: 78 }, { name: "Oil Filter", partNumber: "MB-OF-SPR", quantity: 1, unitPrice: 18, total: 18 }],
  1998. [{ description: "Oil and filter change", hours: 0.5, rate: 110, total: 55 }, { description: "Multi-point inspection", hours: 1.0, rate: 110, total: 110 }]),
  1999. // 25: BMW coolant (Chris)
  2000. sr({ vehicleId: vehicles[4].id, title: "BMW 330i - Coolant Flush", description: "Coolant due. Flush and refill with BMW-approved coolant.", type: "maintenance", status: "pending", serviceDate: day(4), mileage: 52000, techName: "Chris Taylor", shopName: "Egeland Auto" },
  2001. [{ name: "BMW Coolant Concentrate (2L)", partNumber: "BMW-COOL-2L", quantity: 2, unitPrice: 32, total: 64 }, { name: "Thermostat Gasket", partNumber: "BMW-TG-N20", quantity: 1, unitPrice: 12, total: 12 }],
  2002. [{ description: "Coolant flush and refill", hours: 1.0, rate: 120, total: 120 }, { description: "Bleed cooling system", hours: 0.5, rate: 120, total: 60 }]),
  2003. // 26: Ram alignment (Sofia)
  2004. sr({ vehicleId: vehicles[15].id, title: "Ram 1500 - Alignment Check", description: "Pulling to the right after pothole. Full 4-wheel alignment.", type: "maintenance", status: "pending", serviceDate: day(4), mileage: 20000, techName: "Sofia Andersen", shopName: "Egeland Auto" },
  2005. [],
  2006. [{ description: "4-wheel alignment", hours: 1.0, rate: 110, total: 110 }]),
  2007. // 27: Audi spark plugs (Sofia)
  2008. sr({ vehicleId: vehicles[10].id, title: "Audi A4 - Spark Plug Replace", description: "Misfire on cylinder 2. Replace all 4 spark plugs.", type: "repair", status: "pending", serviceDate: day(4), mileage: 62000, techName: "Sofia Andersen", shopName: "Egeland Auto" },
  2009. [{ name: "Spark Plugs NGK (set of 4)", partNumber: "AUD-SP-NGK4", quantity: 1, unitPrice: 68, total: 68 }, { name: "Ignition Coil (cyl 2)", partNumber: "AUD-IC-A4", quantity: 1, unitPrice: 52, total: 52 }],
  2010. [{ description: "Spark plug replacement (4 cyl)", hours: 1.0, rate: 120, total: 120 }, { description: "Diagnose misfire and replace coil", hours: 0.5, rate: 120, total: 60 }],
  2011. "P0302 - misfire cyl 2. Coil swap test confirmed faulty coil on cyl 2. Replace coil and all 4 plugs."),
  2012. // 28: Tesla tire rotation (Sofia)
  2013. sr({ vehicleId: vehicles[7].id, title: "Tesla Model Y - Tire Rotation", description: "Rotate tires front to rear. Check tread depth and tire pressure.", type: "maintenance", status: "pending", serviceDate: day(4), mileage: 33000, techName: "Sofia Andersen", shopName: "Egeland Auto" },
  2014. [],
  2015. [{ description: "Tire rotation front to rear", hours: 0.5, rate: 95, total: 47.50 }, { description: "Tread depth measurement and pressure set", hours: 0.2, rate: 95, total: 19 }]),
  2016. // 29: Massey Ferguson radiator (Erik)
  2017. sr({ vehicleId: vehicles[24].id, title: "Massey Ferguson - Radiator Flush", description: "Overheating under load. Flush radiator and check thermostat.", type: "maintenance", status: "pending", serviceDate: day(4), mileage: 3800, techName: "Erik Haugen", shopName: "Egeland Auto" },
  2018. [{ name: "Coolant (5gal)", partNumber: "MF-COOL-5G", quantity: 1, unitPrice: 65, total: 65 }, { name: "Thermostat", partNumber: "MF-TH-8S", quantity: 1, unitPrice: 48, total: 48 }],
  2019. [{ description: "Radiator flush and refill", hours: 1.5, rate: 130, total: 195 }, { description: "Thermostat replacement", hours: 1.0, rate: 130, total: 130 }]),
  2020. // 30: Mack mirror (Nina)
  2021. sr({ vehicleId: vehicles[19].id, title: "Mack Granite - Mirror Replacement", description: "Left side mirror housing cracked. Replace complete mirror assembly.", type: "repair", status: "pending", serviceDate: day(4), mileage: 157000, techName: "Nina Berglund", shopName: "Egeland Auto" },
  2022. [{ name: "Complete Mirror Assembly (left)", partNumber: "MACK-MA-L-GR", quantity: 1, unitPrice: 285, total: 285 }],
  2023. [{ description: "Mirror assembly replacement", hours: 1.0, rate: 120, total: 120 }]),
  2024. // 31: Ram battery (Kari)
  2025. sr({ vehicleId: vehicles[16].id, title: "Ram 1500 - Battery Replace", description: "Slow cranking. Battery tested bad. Replace with OEM spec.", type: "repair", status: "pending", serviceDate: day(4), mileage: 45500, techName: "Kari Moen", shopName: "Egeland Auto" },
  2026. [{ name: "Battery Group 65 AGM", partNumber: "BAT-G65-AGM", quantity: 1, unitPrice: 185, total: 185 }],
  2027. [{ description: "Battery replacement and terminal cleaning", hours: 0.5, rate: 95, total: 47.50 }, { description: "Charging system test", hours: 0.2, rate: 95, total: 19 }]),
  2028. // === Monday extras to fill the timeline ===
  2029. // 32: Erik - Porsche oil change (Monday)
  2030. sr({ vehicleId: vehicles[13].id, title: "Porsche 911 - Oil Change", description: "Synthetic oil change with OEM filter. Check brake wear sensors.", type: "maintenance", status: "pending", serviceDate: day(0), mileage: 13200, techName: "Erik Haugen", shopName: "Egeland Auto" },
  2031. [{ name: "Porsche Approved Oil 0W-40 (9qt)", partNumber: "POR-0W40-9Q", quantity: 1, unitPrice: 165, total: 165 }, { name: "Oil Filter", partNumber: "POR-OF-992", quantity: 1, unitPrice: 28, total: 28 }],
  2032. [{ description: "Oil and filter change", hours: 1.0, rate: 145, total: 145 }, { description: "Multi-point inspection", hours: 0.5, rate: 145, total: 72.50 }]),
  2033. // 33: Nina - BMW coolant hose (Monday)
  2034. sr({ vehicleId: vehicles[4].id, title: "BMW 330i - Coolant Hose Replacement", description: "Upper radiator hose cracked. Replace hose and top off coolant.", type: "repair", status: "pending", serviceDate: day(0), mileage: 56000, techName: "Nina Berglund", shopName: "Egeland Auto" },
  2035. [{ name: "Upper Radiator Hose", partNumber: "BMW-URH-N20", quantity: 1, unitPrice: 85, total: 85 }, { name: "Coolant G13 (1L)", partNumber: "BMW-COOL-1L", quantity: 2, unitPrice: 18, total: 36 }],
  2036. [{ description: "Radiator hose replacement", hours: 1.5, rate: 120, total: 180 }, { description: "Coolant bleed and pressure test", hours: 0.5, rate: 120, total: 60 }]),
  2037. // 34: Nina - Jeep brake inspection (Monday)
  2038. sr({ vehicleId: vehicles[11].id, title: "Jeep Wrangler - Brake Inspection", description: "Customer reports squealing. Inspect pads, rotors, and calipers.", type: "inspection", status: "pending", serviceDate: day(0), mileage: 29000, techName: "Nina Berglund", shopName: "Egeland Auto" },
  2039. [],
  2040. [{ description: "Full brake system inspection", hours: 1.0, rate: 110, total: 110 }, { description: "Document findings and recommend", hours: 0.5, rate: 110, total: 55 }]),
  2041. // 35: Kari - Fendt hydraulic filter (Monday)
  2042. sr({ vehicleId: vehicles[23].id, title: "Fendt 942 - Hydraulic Filter Service", description: "Scheduled hydraulic filter change. Check system pressure.", type: "maintenance", status: "pending", serviceDate: day(0), mileage: 1250, techName: "Kari Moen", shopName: "Egeland Auto" },
  2043. [{ name: "Hydraulic Filter Set", partNumber: "FENDT-HFS-942", quantity: 1, unitPrice: 165, total: 165 }, { name: "Hydraulic Oil Sample Kit", partNumber: "OIL-SAMPLE", quantity: 1, unitPrice: 15, total: 15 }],
  2044. [{ description: "Hydraulic filter replacement", hours: 1.5, rate: 150, total: 225 }, { description: "Pressure test and oil sample", hours: 1.0, rate: 150, total: 150 }]),
  2045. // 36: Kari - Excavator grease service (Monday)
  2046. sr({ vehicleId: vehicles[31].id, title: "Komatsu PC210 - Full Grease Service", description: "Complete grease service all 24 fittings. Check pin play.", type: "maintenance", status: "pending", serviceDate: day(0), mileage: 3150, techName: "Kari Moen", shopName: "Egeland Auto" },
  2047. [{ name: "Grease Cartridges (box of 10)", partNumber: "GRZ-EP2-10", quantity: 1, unitPrice: 42, total: 42 }],
  2048. [{ description: "Grease all fittings (24 points)", hours: 2.0, rate: 130, total: 260 }, { description: "Pin and bushing play inspection", hours: 0.5, rate: 130, total: 65 }]),
  2049. // 37: Daniel - Volvo FH brake adjustment (Monday)
  2050. sr({ vehicleId: vehicles[18].id, title: "Volvo FH 640 - Brake Adjustment", description: "Annual brake adjustment. Check linings and drums.", type: "maintenance", status: "pending", serviceDate: day(0), mileage: 93000, techName: "Daniel Eriksen", shopName: "Egeland Auto" },
  2051. [{ name: "Brake Lining Wear Indicators (set)", partNumber: "VOL-BLWI-FH", quantity: 1, unitPrice: 45, total: 45 }],
  2052. [{ description: "Brake adjustment all axles", hours: 2.5, rate: 140, total: 350 }, { description: "Lining measurement and documentation", hours: 1.0, rate: 140, total: 140 }]),
  2053. // 38: Daniel - Dump truck hydraulic hose (Monday)
  2054. sr({ vehicleId: vehicles[21].id, title: "Dump Truck - Hydraulic Hose Replace", description: "Hose bulging near fitting. Replace before failure.", type: "repair", status: "pending", serviceDate: day(0), mileage: 83000, techName: "Daniel Eriksen", shopName: "Egeland Auto" },
  2055. [{ name: "Hydraulic Hose Assembly 3/4\"", partNumber: "HYD-HA-34", quantity: 2, unitPrice: 120, total: 240 }, { name: "Hose Fittings (4-pack)", partNumber: "HYD-FIT-4", quantity: 1, unitPrice: 65, total: 65 }],
  2056. [{ description: "Hose removal and replacement", hours: 2.0, rate: 130, total: 260 }, { description: "System bleed and pressure test", hours: 0.5, rate: 130, total: 65 }]),
  2057. // 39: Lars - Kenworth air filter (Monday)
  2058. sr({ vehicleId: vehicles[17].id, title: "Kenworth T680 - Air Filter Service", description: "Replace primary and secondary air filters. Clean intake ducting.", type: "maintenance", status: "pending", serviceDate: day(0), mileage: 186500, techName: "Lars Johansen", shopName: "Egeland Auto" },
  2059. [{ name: "Primary Air Filter", partNumber: "KW-PAF-T680", quantity: 1, unitPrice: 85, total: 85 }, { name: "Secondary Air Filter", partNumber: "KW-SAF-T680", quantity: 1, unitPrice: 55, total: 55 }],
  2060. [{ description: "Air filter replacement (both)", hours: 0.5, rate: 130, total: 65 }, { description: "Intake system inspection and clean", hours: 0.5, rate: 130, total: 65 }]),
  2061. // 40: Sofia - Honda Civic tire rotation (Monday)
  2062. sr({ vehicleId: vehicles[5].id, title: "Honda Civic - Tire Rotation + Check", description: "Rotate tires. Customer also reports slight vibration at highway speed.", type: "maintenance", status: "pending", serviceDate: day(0), mileage: 68500, techName: "Sofia Andersen", shopName: "Egeland Auto" },
  2063. [],
  2064. [{ description: "Tire rotation", hours: 0.5, rate: 95, total: 47.50 }, { description: "Wheel balance check (4 wheels)", hours: 0.5, rate: 95, total: 47.50 }, { description: "Road test for vibration", hours: 0.3, rate: 95, total: 28.50 }]),
  2065. ]);
  2066. // Unassigned jobs (NOT on the board - will show in unassigned panel)
  2067. console.log("\nCreating unassigned service records...");
  2068. const unassignedRecords = await Promise.all([
  2069. sr({ vehicleId: vehicles[0].id, title: "Toyota Camry - Check Engine Light", description: "P0171 - System Too Lean. Diagnose and repair. Customer reports rough idle.", type: "repair", status: "pending", mileage: 16200, shopName: "Egeland Auto" },
  2070. [{ name: "MAF Sensor", partNumber: "TOY-MAF-CAM", quantity: 1, unitPrice: 145, total: 145 }, { name: "Air Filter", partNumber: "TOY-AF-CAM", quantity: 1, unitPrice: 18, total: 18 }],
  2071. [{ description: "Diagnostic scan and smoke test", hours: 1.0, rate: 110, total: 110 }, { description: "MAF sensor replacement", hours: 0.5, rate: 110, total: 55 }],
  2072. "P0171 lean code. Inspected intake for vacuum leaks - none found. MAF sensor reading low. Suspect contaminated MAF."),
  2073. sr({ vehicleId: vehicles[9].id, title: "Sprinter - Sliding Door Repair", description: "Sliding door sticking halfway. Roller mechanism needs replacement.", type: "repair", status: "pending", mileage: 36500, shopName: "Egeland Auto" },
  2074. [{ name: "Sliding Door Roller Assembly", partNumber: "MB-SDRA-SPR", quantity: 1, unitPrice: 220, total: 220 }, { name: "Door Track Lubricant", partNumber: "LUB-DTL-1", quantity: 1, unitPrice: 15, total: 15 }],
  2075. [{ description: "Roller mechanism removal and replacement", hours: 2.0, rate: 110, total: 220 }]),
  2076. sr({ vehicleId: vehicles[13].id, title: "Porsche 911 - Pre-Track Inspection", description: "Full safety inspection before April track day. Brakes, fluids, suspension, tires.", type: "inspection", status: "pending", mileage: 13000, shopName: "Egeland Auto" },
  2077. [{ name: "Brake Fluid DOT4 Racing (1L)", partNumber: "POR-BF-R-1L", quantity: 2, unitPrice: 48, total: 96 }],
  2078. [{ description: "Full multi-point track safety inspection", hours: 2.0, rate: 145, total: 290 }, { description: "Brake fluid flush with racing fluid", hours: 1.5, rate: 145, total: 217.50 }]),
  2079. sr({ vehicleId: vehicles[19].id, title: "Mack Granite - Dump Body Weld Repair", description: "Cracks in dump body floor. Weld repair and reinforce high-stress areas.", type: "repair", status: "pending", mileage: 156000, shopName: "Egeland Auto" },
  2080. [{ name: "Steel Plate 3/8\" (2 sheets)", partNumber: "STL-PLT-38", quantity: 2, unitPrice: 120, total: 240 }, { name: "Welding Wire (10lb)", partNumber: "WLD-WIRE-10", quantity: 1, unitPrice: 45, total: 45 }],
  2081. [{ description: "Grind and prep crack areas", hours: 2.0, rate: 130, total: 260 }, { description: "Weld repair and reinforce", hours: 4.0, rate: 130, total: 520 }]),
  2082. sr({ vehicleId: vehicles[22].id, title: "John Deere 6R - Front Loader Pins", description: "Excessive play in front loader attachment pins. Replace pins and bushings.", type: "repair", status: "pending", mileage: 2800, shopName: "Egeland Auto" },
  2083. [{ name: "Loader Pin Kit", partNumber: "JD-LPK-6R", quantity: 1, unitPrice: 185, total: 185 }, { name: "Bronze Bushings (set of 4)", partNumber: "JD-BB-6R-4", quantity: 1, unitPrice: 120, total: 120 }],
  2084. [{ description: "Pin and bushing removal", hours: 2.0, rate: 140, total: 280 }, { description: "New bushing press-in and pin installation", hours: 2.0, rate: 140, total: 280 }]),
  2085. sr({ vehicleId: vehicles[31].id, title: "Komatsu PC210 - Boom Cylinder Reseal", description: "Slow drift on boom cylinder. Reseal required.", type: "repair", status: "pending", mileage: 3200, shopName: "Egeland Auto" },
  2086. [{ name: "Boom Cylinder Seal Kit", partNumber: "KOM-BCSK-PC210", quantity: 1, unitPrice: 320, total: 320 }, { name: "Hydraulic Oil (5gal)", partNumber: "HYD-ISO46-5G", quantity: 1, unitPrice: 95, total: 95 }],
  2087. [{ description: "Boom cylinder removal", hours: 3.0, rate: 140, total: 420 }, { description: "Reseal and reinstall", hours: 2.5, rate: 140, total: 350 }]),
  2088. sr({ vehicleId: vehicles[36].id, title: "Liebherr LTM 1100 - Outrigger Service", description: "Right rear outrigger slow to extend. Inspect valve and cylinder seals.", type: "repair", status: "pending", mileage: 4200, shopName: "Egeland Auto" },
  2089. [{ name: "Outrigger Cylinder Seal Kit", partNumber: "LIE-OCSK-1100", quantity: 1, unitPrice: 450, total: 450 }, { name: "Outrigger Control Valve", partNumber: "LIE-OCV-1100", quantity: 1, unitPrice: 680, total: 680 }],
  2090. [{ description: "Diagnose outrigger hydraulic circuit", hours: 2.0, rate: 150, total: 300 }, { description: "Cylinder reseal or valve replacement", hours: 4.0, rate: 150, total: 600 }]),
  2091. sr({ vehicleId: vehicles[6].id, title: "Tesla Model 3 - 12V Battery Replace", description: "Low voltage warning. Replace 12V auxiliary battery.", type: "repair", status: "pending", mileage: 8200, shopName: "Egeland Auto" },
  2092. [{ name: "12V Lithium Battery (Tesla OEM)", partNumber: "TES-12V-M3", quantity: 1, unitPrice: 110, total: 110 }],
  2093. [{ description: "12V battery replacement", hours: 0.5, rate: 110, total: 55 }]),
  2094. sr({ vehicleId: vehicles[23].id, title: "Fendt 942 - GPS Autosteer Calibration", description: "Autosteer drifting right by 6 inches. Recalibrate GPS receiver and steering controller.", type: "maintenance", status: "pending", mileage: 1200, shopName: "Egeland Auto" },
  2095. [],
  2096. [{ description: "GPS receiver calibration", hours: 1.5, rate: 150, total: 225 }, { description: "Steering controller calibration and field test", hours: 2.0, rate: 150, total: 300 }],
  2097. "RTK base station signal verified. GPS receiver antenna checked - mounting bracket slightly loose causing drift. Tighten and recalibrate."),
  2098. sr({ vehicleId: vehicles[33].id, title: "Volvo EC220E - Bucket Teeth Replace", description: "3 worn bucket teeth. Replace full set of 5 for even wear.", type: "maintenance", status: "pending", mileage: 1600, shopName: "Egeland Auto" },
  2099. [{ name: "Bucket Teeth Set (5)", partNumber: "VOL-BTS-EC220", quantity: 1, unitPrice: 340, total: 340 }, { name: "Retainer Pins (5-pack)", partNumber: "VOL-RP-5", quantity: 1, unitPrice: 45, total: 45 }],
  2100. [{ description: "Bucket teeth removal and replacement", hours: 1.5, rate: 130, total: 195 }]),
  2101. sr({ vehicleId: vehicles[18].id, title: "Volvo FH 640 - Turbo Boost Leak", description: "Low power complaint. Suspect charge air cooler pipe leak. Diagnose.", type: "repair", status: "pending", mileage: 93500, shopName: "Egeland Auto" },
  2102. [{ name: "Charge Air Cooler Pipe + Clamps", partNumber: "VOL-CACP-FH", quantity: 1, unitPrice: 185, total: 185 }],
  2103. [{ description: "Boost leak diagnosis and smoke test", hours: 1.5, rate: 140, total: 210 }, { description: "Pipe replacement and clamp tightening", hours: 1.0, rate: 140, total: 140 }],
  2104. "Smoke test confirmed leak at charge air cooler pipe connection. Clamp was loose and pipe boot cracked."),
  2105. sr({ vehicleId: vehicles[17].id, title: "Kenworth T680 - Sleeper AC Repair", description: "Sleeper AC not cooling. Likely low refrigerant or compressor issue.", type: "repair", status: "waiting-parts", mileage: 186000, shopName: "Egeland Auto" },
  2106. [{ name: "Sleeper AC Compressor", partNumber: "KW-SAC-T680", quantity: 1, unitPrice: 580, total: 580 }, { name: "Refrigerant R-134a (3 cans)", partNumber: "REF-134A-3", quantity: 1, unitPrice: 65, total: 65 }, { name: "AC Condenser", partNumber: "KW-COND-T680", quantity: 1, unitPrice: 320, total: 320 }],
  2107. [{ description: "Sleeper AC system diagnosis", hours: 1.5, rate: 140, total: 210 }, { description: "Compressor and condenser replacement", hours: 3.5, rate: 140, total: 490 }, { description: "Evacuate, recharge and leak test", hours: 1.0, rate: 140, total: 140 }],
  2108. "Compressor seized. Condenser contaminated with metal debris from failed compressor. Both need replacement. Parts on order - ETA Wednesday."),
  2109. sr({ vehicleId: vehicles[38].id, title: "Corvette C8 - Pre-Track Brake Service", description: "Customer has track day in 2 weeks. Flush brake fluid, inspect pads, check tire wear.", type: "maintenance", status: "pending", mileage: 6200, shopName: "Egeland Auto" },
  2110. [{ name: "Brake Fluid DOT4 Racing (1L)", partNumber: "BF-DOT4R-1L", quantity: 3, unitPrice: 48, total: 144 }],
  2111. [{ description: "Brake fluid flush all 4 corners", hours: 1.5, rate: 130, total: 195 }, { description: "Brake pad measurement and inspection", hours: 0.5, rate: 130, total: 65 }, { description: "Tire tread depth and pressure check", hours: 0.3, rate: 130, total: 39 }]),
  2112. sr({ vehicleId: vehicles[39].id, title: "Defender 110 - Air Suspension Warning", description: "Dashboard warning light for air suspension. Possible compressor or height sensor issue.", type: "repair", status: "pending", mileage: 28500, shopName: "Egeland Auto" },
  2113. [{ name: "Air Suspension Height Sensor", partNumber: "LR-ASHS-DEF", quantity: 1, unitPrice: 210, total: 210 }],
  2114. [{ description: "Diagnose air suspension fault codes", hours: 1.0, rate: 120, total: 120 }, { description: "Height sensor replacement and calibration", hours: 1.5, rate: 120, total: 180 }]),
  2115. sr({ vehicleId: vehicles[40].id, title: "Charger R/T - Rear Alignment + Tire Replace", description: "Inner edge tire wear on rears. Alignment and new rear tires needed.", type: "repair", status: "pending", mileage: 35000, shopName: "Egeland Auto" },
  2116. [{ name: "Firestone Firehawk Indy 500 275/40R20 (pair)", partNumber: "FS-FH500-27540-2", quantity: 1, unitPrice: 380, total: 380 }],
  2117. [{ description: "Rear tire mount and balance (2)", hours: 0.5, rate: 95, total: 47.50 }, { description: "4-wheel alignment", hours: 1.0, rate: 110, total: 110 }]),
  2118. sr({ vehicleId: vehicles[41].id, title: "WRX - Clutch Adjustment + Inspection", description: "Customer reports high clutch engagement point. Inspect clutch wear and adjust.", type: "repair", status: "pending", mileage: 12400, shopName: "Egeland Auto" },
  2119. [{ name: "Clutch Master Cylinder Fluid", partNumber: "SUB-CMF-DOT4", quantity: 1, unitPrice: 15, total: 15 }],
  2120. [{ description: "Clutch pedal adjustment and bleed", hours: 0.5, rate: 110, total: 55 }, { description: "Clutch wear inspection via inspection port", hours: 0.5, rate: 110, total: 55 }],
  2121. "Clutch engagement point higher than normal but disc still within spec. Bled hydraulic line - air found. Pedal feel improved. Monitor."),
  2122. ]);
  2123. console.log(` Created ${unassignedRecords.length} unassigned service records`);
  2124. const jobUpdates = await Promise.all([
  2125. // ============ MONDAY (day 0) - 2026-03-09 - Primary showcase day ============
  2126. // Jake Wilson [0]: Honda AC (8:00-11:30) + F-150 trans flush (12:30-15:00)
  2127. prisma.serviceRecord.update({ where: { id: boardServiceRecords[0].id }, data: { technicianId: technicians[0].id, sortOrder: 0, startDateTime: dt(0, 8, 0), endDateTime: dt(0, 11, 30) } }),
  2128. prisma.serviceRecord.update({ where: { id: serviceRecords[18].id }, data: { technicianId: technicians[0].id, sortOrder: 1, startDateTime: dt(0, 12, 30), endDateTime: dt(0, 15, 0) } }),
  2129. // Chris Taylor [1]: Audi timing belt (7:30-13:00)
  2130. prisma.serviceRecord.update({ where: { id: boardServiceRecords[1].id }, data: { technicianId: technicians[1].id, sortOrder: 0, startDateTime: dt(0, 7, 30), endDateTime: dt(0, 13, 0) } }),
  2131. // Marcus Reed [2]: CAT 745 transmission rebuild (7:00-16:00, multi-day spans Mon-Wed)
  2132. prisma.serviceRecord.update({ where: { id: boardServiceRecords[6].id }, data: { technicianId: technicians[2].id, sortOrder: 0, startDateTime: dt(0, 7, 0), endDateTime: dt(2, 16, 0) } }),
  2133. // Sofia Andersen [3]: Tesla suspension (9:00-11:30)
  2134. prisma.serviceRecord.update({ where: { id: boardServiceRecords[2].id }, data: { technicianId: technicians[3].id, sortOrder: 0, startDateTime: dt(0, 9, 0), endDateTime: dt(0, 11, 30) } }),
  2135. // Lars Johansen [5]: Ram oil change (8:00-9:00)
  2136. prisma.serviceRecord.update({ where: { id: boardServiceRecords[11].id }, data: { technicianId: technicians[5].id, sortOrder: 0, startDateTime: dt(0, 8, 0), endDateTime: dt(0, 9, 0) } }),
  2137. // Tom Bradley [7]: Concrete mixer drum motor (7:30-14:30)
  2138. prisma.serviceRecord.update({ where: { id: boardServiceRecords[13].id }, data: { technicianId: technicians[7].id, sortOrder: 0, startDateTime: dt(0, 7, 30), endDateTime: dt(0, 14, 30) } }),
  2139. prisma.serviceRecord.update({ where: { id: boardServiceRecords[32].id }, data: { technicianId: technicians[4].id, sortOrder: 1, startDateTime: dt(0, 11, 0), endDateTime: dt(0, 12, 30) } }),
  2140. // Nina Berglund [6]: BMW coolant hose (7:30-9:30) + Jeep brake inspection (10:00-11:30)
  2141. prisma.serviceRecord.update({ where: { id: boardServiceRecords[33].id }, data: { technicianId: technicians[6].id, sortOrder: 0, startDateTime: dt(0, 7, 30), endDateTime: dt(0, 9, 30) } }),
  2142. prisma.serviceRecord.update({ where: { id: boardServiceRecords[34].id }, data: { technicianId: technicians[6].id, sortOrder: 1, startDateTime: dt(0, 10, 0), endDateTime: dt(0, 11, 30) } }),
  2143. // Kari Moen [8]: Fendt hydraulic filter (8:00-10:30) + Komatsu grease (11:00-13:30)
  2144. prisma.serviceRecord.update({ where: { id: boardServiceRecords[35].id }, data: { technicianId: technicians[8].id, sortOrder: 0, startDateTime: dt(0, 8, 0), endDateTime: dt(0, 10, 30) } }),
  2145. prisma.serviceRecord.update({ where: { id: boardServiceRecords[36].id }, data: { technicianId: technicians[8].id, sortOrder: 1, startDateTime: dt(0, 11, 0), endDateTime: dt(0, 13, 30) } }),
  2146. // Daniel Eriksen [9]: Volvo brake adjustment (7:30-11:00) + Dump truck hose (11:30-14:00)
  2147. prisma.serviceRecord.update({ where: { id: boardServiceRecords[37].id }, data: { technicianId: technicians[9].id, sortOrder: 0, startDateTime: dt(0, 7, 30), endDateTime: dt(0, 11, 0) } }),
  2148. prisma.serviceRecord.update({ where: { id: boardServiceRecords[38].id }, data: { technicianId: technicians[9].id, sortOrder: 1, startDateTime: dt(0, 11, 30), endDateTime: dt(0, 14, 0) } }),
  2149. prisma.serviceRecord.update({ where: { id: boardServiceRecords[39].id }, data: { technicianId: technicians[5].id, sortOrder: 2, startDateTime: dt(0, 10, 30), endDateTime: dt(0, 11, 30) } }),
  2150. // Sofia Andersen [3]: (already has Tesla 9-11:30) + Honda tire rotation (12:00-13:15)
  2151. prisma.serviceRecord.update({ where: { id: boardServiceRecords[40].id }, data: { technicianId: technicians[3].id, sortOrder: 1, startDateTime: dt(0, 12, 0), endDateTime: dt(0, 13, 15) } }),
  2152. // ============ TUESDAY (day 1) ============
  2153. // Jake [0]: Silverado 60K (8:00-11:30)
  2154. prisma.serviceRecord.update({ where: { id: boardServiceRecords[3].id }, data: { technicianId: technicians[0].id, sortOrder: 0, startDateTime: dt(1, 8, 0), endDateTime: dt(1, 11, 30) } }),
  2155. // Chris [1]: Sprinter turbo (7:30-13:00)
  2156. prisma.serviceRecord.update({ where: { id: serviceRecords[12].id }, data: { technicianId: technicians[1].id, sortOrder: 0, startDateTime: dt(1, 7, 30), endDateTime: dt(1, 13, 0) } }),
  2157. // Marcus [2]: CAT 745 continued (multi-day job spans from Monday)
  2158. // Erik [4]: Tacoma leaf springs (8:00-11:30)
  2159. prisma.serviceRecord.update({ where: { id: boardServiceRecords[4].id }, data: { technicianId: technicians[4].id, sortOrder: 0, startDateTime: dt(1, 8, 0), endDateTime: dt(1, 11, 30) } }),
  2160. // Nina [6]: F-150 wheel bearing (9:00-11:30)
  2161. prisma.serviceRecord.update({ where: { id: boardServiceRecords[12].id }, data: { technicianId: technicians[6].id, sortOrder: 0, startDateTime: dt(1, 9, 0), endDateTime: dt(1, 11, 30) } }),
  2162. // Daniel [9]: Dump truck tailgate (8:00-12:00)
  2163. prisma.serviceRecord.update({ where: { id: boardServiceRecords[15].id }, data: { technicianId: technicians[9].id, sortOrder: 0, startDateTime: dt(1, 8, 0), endDateTime: dt(1, 12, 0) } }),
  2164. // ============ WEDNESDAY (day 2) ============
  2165. // Jake [0]: F-150 exhaust (8:00-11:00)
  2166. prisma.serviceRecord.update({ where: { id: boardServiceRecords[7].id }, data: { technicianId: technicians[0].id, sortOrder: 0, startDateTime: dt(2, 8, 0), endDateTime: dt(2, 11, 0) } }),
  2167. // Marcus [2]: Bobcat annual (13:00-15:30, after CAT 745 wraps up)
  2168. prisma.serviceRecord.update({ where: { id: boardServiceRecords[5].id }, data: { technicianId: technicians[2].id, sortOrder: 1, startDateTime: dt(2, 13, 0), endDateTime: dt(2, 15, 30) } }),
  2169. // Chris [1]: Volvo EC220E (7:30-13:00)
  2170. prisma.serviceRecord.update({ where: { id: serviceRecords[10].id }, data: { technicianId: technicians[1].id, sortOrder: 0, startDateTime: dt(2, 7, 30), endDateTime: dt(2, 13, 0) } }),
  2171. // Kari [8]: Tractor PTO clutch (8:30-10:30)
  2172. prisma.serviceRecord.update({ where: { id: boardServiceRecords[14].id }, data: { technicianId: technicians[8].id, sortOrder: 0, startDateTime: dt(2, 8, 30), endDateTime: dt(2, 10, 30) } }),
  2173. // Nina [6]: VW Golf clutch (7:30-14:30)
  2174. prisma.serviceRecord.update({ where: { id: boardServiceRecords[17].id }, data: { technicianId: technicians[6].id, sortOrder: 0, startDateTime: dt(2, 7, 30), endDateTime: dt(2, 14, 30) } }),
  2175. // ============ THURSDAY (day 3) ============
  2176. // Sofia [3]: Ram transfer case (9:00-10:30)
  2177. prisma.serviceRecord.update({ where: { id: boardServiceRecords[8].id }, data: { technicianId: technicians[3].id, sortOrder: 0, startDateTime: dt(3, 9, 0), endDateTime: dt(3, 10, 30) } }),
  2178. // Jake [0]: Volvo FH DOT inspection (7:30-13:30)
  2179. prisma.serviceRecord.update({ where: { id: serviceRecords[5].id }, data: { technicianId: technicians[0].id, sortOrder: 0, startDateTime: dt(3, 7, 30), endDateTime: dt(3, 13, 30) } }),
  2180. // Lars [5]: Forklift mast chain (8:00-11:30)
  2181. prisma.serviceRecord.update({ where: { id: boardServiceRecords[16].id }, data: { technicianId: technicians[5].id, sortOrder: 0, startDateTime: dt(3, 8, 0), endDateTime: dt(3, 11, 30) } }),
  2182. // Kari [8]: Kenworth clutch adjust (8:00-10:00)
  2183. prisma.serviceRecord.update({ where: { id: boardServiceRecords[19].id }, data: { technicianId: technicians[8].id, sortOrder: 0, startDateTime: dt(3, 8, 0), endDateTime: dt(3, 10, 0) } }),
  2184. // ============ FRIDAY (day 4) ============
  2185. // Jake [0] (3 jobs): Camry cabin filter (8:00-8:30), Honda brakes (8:45-10:00), Silverado taillight (10:15-10:45)
  2186. prisma.serviceRecord.update({ where: { id: boardServiceRecords[21].id }, data: { technicianId: technicians[0].id, sortOrder: 0, startDateTime: dt(4, 8, 0), endDateTime: dt(4, 8, 30) } }),
  2187. prisma.serviceRecord.update({ where: { id: boardServiceRecords[22].id }, data: { technicianId: technicians[0].id, sortOrder: 1, startDateTime: dt(4, 8, 45), endDateTime: dt(4, 10, 0) } }),
  2188. prisma.serviceRecord.update({ where: { id: boardServiceRecords[23].id }, data: { technicianId: technicians[0].id, sortOrder: 2, startDateTime: dt(4, 10, 15), endDateTime: dt(4, 10, 45) } }),
  2189. // Chris [1] (3 jobs): Jeep axle seal (7:30-10:30), Sprinter oil (11:00-12:30), BMW coolant (13:00-14:30)
  2190. prisma.serviceRecord.update({ where: { id: boardServiceRecords[10].id }, data: { technicianId: technicians[1].id, sortOrder: 0, startDateTime: dt(4, 7, 30), endDateTime: dt(4, 10, 30) } }),
  2191. prisma.serviceRecord.update({ where: { id: boardServiceRecords[24].id }, data: { technicianId: technicians[1].id, sortOrder: 1, startDateTime: dt(4, 11, 0), endDateTime: dt(4, 12, 30) } }),
  2192. prisma.serviceRecord.update({ where: { id: boardServiceRecords[25].id }, data: { technicianId: technicians[1].id, sortOrder: 2, startDateTime: dt(4, 13, 0), endDateTime: dt(4, 14, 30) } }),
  2193. // Marcus [2]: Combine pre-season (7:00-15:00)
  2194. prisma.serviceRecord.update({ where: { id: boardServiceRecords[9].id }, data: { technicianId: technicians[2].id, sortOrder: 0, startDateTime: dt(4, 7, 0), endDateTime: dt(4, 15, 0) } }),
  2195. // Sofia [3] (3 jobs): Ram alignment (8:00-9:00), Audi spark plugs (9:30-11:00), Tesla tire rotation (11:30-12:15)
  2196. prisma.serviceRecord.update({ where: { id: boardServiceRecords[26].id }, data: { technicianId: technicians[3].id, sortOrder: 0, startDateTime: dt(4, 8, 0), endDateTime: dt(4, 9, 0) } }),
  2197. prisma.serviceRecord.update({ where: { id: boardServiceRecords[27].id }, data: { technicianId: technicians[3].id, sortOrder: 1, startDateTime: dt(4, 9, 30), endDateTime: dt(4, 11, 0) } }),
  2198. prisma.serviceRecord.update({ where: { id: boardServiceRecords[28].id }, data: { technicianId: technicians[3].id, sortOrder: 2, startDateTime: dt(4, 11, 30), endDateTime: dt(4, 12, 15) } }),
  2199. // Erik [4]: Massey Ferguson radiator (8:00-10:30)
  2200. prisma.serviceRecord.update({ where: { id: boardServiceRecords[29].id }, data: { technicianId: technicians[4].id, sortOrder: 0, startDateTime: dt(4, 8, 0), endDateTime: dt(4, 10, 30) } }),
  2201. // Nina [6]: Mack mirror replacement (9:00-10:00)
  2202. prisma.serviceRecord.update({ where: { id: boardServiceRecords[30].id }, data: { technicianId: technicians[6].id, sortOrder: 0, startDateTime: dt(4, 9, 0), endDateTime: dt(4, 10, 0) } }),
  2203. // Tom [7]: Combine header knives (7:30-11:30)
  2204. prisma.serviceRecord.update({ where: { id: boardServiceRecords[18].id }, data: { technicianId: technicians[7].id, sortOrder: 0, startDateTime: dt(4, 7, 30), endDateTime: dt(4, 11, 30) } }),
  2205. // Kari [8]: Ram battery replace (8:00-8:45)
  2206. prisma.serviceRecord.update({ where: { id: boardServiceRecords[31].id }, data: { technicianId: technicians[8].id, sortOrder: 0, startDateTime: dt(4, 8, 0), endDateTime: dt(4, 8, 45) } }),
  2207. // Daniel [9]: Jeep steering stabilizer (8:00-10:00)
  2208. prisma.serviceRecord.update({ where: { id: boardServiceRecords[20].id }, data: { technicianId: technicians[9].id, sortOrder: 0, startDateTime: dt(4, 8, 0), endDateTime: dt(4, 10, 0) } }),
  2209. ]);
  2210. console.log(` Updated ${jobUpdates.length} service records with technician assignments and time slots`);
  2211. // -- Assign the Demo Owner a few active jobs so the dashboard "My Active Jobs" populates --
  2212. console.log("\nAssigning active jobs to the Demo Owner technician...");
  2213. const demoOwnerTech = technicians[10]; // Demo Owner
  2214. const demoOwnerJobs = await Promise.all([
  2215. // Tesla Model Y suspension (pending) — reassign from Sofia
  2216. prisma.serviceRecord.update({ where: { id: boardServiceRecords[2].id }, data: { technicianId: demoOwnerTech.id } }),
  2217. // F-150 exhaust leak repair (pending) — reassign from Jake
  2218. prisma.serviceRecord.update({ where: { id: boardServiceRecords[7].id }, data: { technicianId: demoOwnerTech.id } }),
  2219. // Jeep axle seal replacement (pending) — reassign from Chris
  2220. prisma.serviceRecord.update({ where: { id: boardServiceRecords[10].id }, data: { technicianId: demoOwnerTech.id } }),
  2221. // Kenworth T680 Sleeper AC repair (waiting-parts) — previously unassigned
  2222. prisma.serviceRecord.update({ where: { id: unassignedRecords[11].id }, data: { technicianId: demoOwnerTech.id } }),
  2223. // Porsche 911 Pre-Track Inspection (pending) — previously unassigned
  2224. prisma.serviceRecord.update({ where: { id: unassignedRecords[2].id }, data: { technicianId: demoOwnerTech.id } }),
  2225. ]);
  2226. console.log(` Assigned ${demoOwnerJobs.length} active jobs to ${demoOwnerTech.name}`);
  2227. // -- Jobs either side of the board week --
  2228. // The showcase week is dense on purpose, but the calendar's month view, the
  2229. // revenue chart and every "this month" counter need work before and after it
  2230. // too. `wd` keeps each job on a weekday by counting from the board's Monday.
  2231. console.log("\nCreating jobs across the surrounding weeks...");
  2232. const wd = (weekOffset: number, dayIdx: number) => MONDAY_OFFSET + weekOffset * 7 + dayIdx;
  2233. type SpreadJob = { w: number; d: number; v: number; tech: number; title: string; desc: string; type: string; hour: number; hours: number; mileage: number; rate: number; parts: { name: string; partNumber: string; quantity: number; unitPrice: number; total: number }[] };
  2234. const spreadJobs: SpreadJob[] = [
  2235. // ── Three weeks back ──
  2236. { w: -3, d: 0, v: 0, tech: 0, title: "Toyota Camry - Wheel Alignment", desc: "Pulling left after kerb strike. Four-wheel alignment.", type: "maintenance", hour: 8, hours: 1.0, mileage: 17800, rate: 95, parts: [] },
  2237. { w: -3, d: 1, v: 12, tech: 4, title: "Jeep Wrangler - Soft Top Swap", desc: "Seasonal swap from hard top to soft top. Inspect seals.", type: "maintenance", hour: 9, hours: 1.5, mileage: 14800, rate: 95, parts: [{ name: "Top Seal Kit", partNumber: "JEEP-TSK-JL", quantity: 1, unitPrice: 65, total: 65 }] },
  2238. { w: -3, d: 2, v: 21, tech: 2, title: "Dump Truck - Annual DOT Inspection", desc: "Full DOT annual. Brakes, lights, suspension, documentation.", type: "inspection", hour: 7, hours: 5.0, mileage: 80500, rate: 140, parts: [{ name: "Marker Light Bulbs (pack of 10)", partNumber: "DMP-MLB-10", quantity: 1, unitPrice: 35, total: 35 }] },
  2239. { w: -3, d: 3, v: 6, tech: 3, title: "Tesla Model 3 - Cabin Filter", desc: "HEPA cabin filter replacement and A/C system check.", type: "maintenance", hour: 10, hours: 1.0, mileage: 7900, rate: 110, parts: [{ name: "HEPA Cabin Filter", partNumber: "TES-CF-M3", quantity: 1, unitPrice: 78, total: 78 }] },
  2240. { w: -3, d: 4, v: 30, tech: 2, title: "CAT D8T - Final Drive Oil Change", desc: "Both final drives drained, flushed and refilled.", type: "maintenance", hour: 7, hours: 4.0, mileage: 5700, rate: 150, parts: [{ name: "Final Drive Oil (15gal)", partNumber: "CAT-FDO-15G", quantity: 1, unitPrice: 420, total: 420 }] },
  2241. // ── Two weeks back ──
  2242. { w: -2, d: 0, v: 5, tech: 0, title: "Honda Civic - Cabin Filter + A/C Clean", desc: "Musty smell reported. Evaporator clean and new cabin filter.", type: "maintenance", hour: 8, hours: 1.5, mileage: 67200, rate: 95, parts: [{ name: "Cabin Filter", partNumber: "HON-CF-CIV", quantity: 1, unitPrice: 24, total: 24 }, { name: "Evaporator Cleaner", partNumber: "AC-EVAP-CLN", quantity: 1, unitPrice: 32, total: 32 }] },
  2243. { w: -2, d: 1, v: 26, tech: 8, title: "New Holland T7 - 500hr Service", desc: "Scheduled interval. Engine oil, filters, hydraulic sample.", type: "maintenance", hour: 8, hours: 3.5, mileage: 2050, rate: 140, parts: [{ name: "Filter Kit T7", partNumber: "NH-FK-T7", quantity: 1, unitPrice: 240, total: 240 }, { name: "Engine Oil 15W-40 (5gal)", partNumber: "NH-15W40-5G", quantity: 1, unitPrice: 185, total: 185 }] },
  2244. { w: -2, d: 2, v: 10, tech: 1, title: "Audi A4 - Haldex Service", desc: "Quattro rear differential fluid and filter at 40K interval.", type: "maintenance", hour: 9, hours: 1.5, mileage: 21600, rate: 120, parts: [{ name: "Haldex Fluid + Filter", partNumber: "AUD-HLD-KIT", quantity: 1, unitPrice: 145, total: 145 }] },
  2245. { w: -2, d: 3, v: 33, tech: 2, title: "Volvo EC220E - Swing Bearing Grease", desc: "Swing bearing and slew ring service, play measured.", type: "maintenance", hour: 7, hours: 2.5, mileage: 1450, rate: 140, parts: [{ name: "Grease Cartridges (box of 10)", partNumber: "GRZ-EP2-10", quantity: 2, unitPrice: 42, total: 84 }] },
  2246. { w: -2, d: 4, v: 1, tech: 0, title: "F-150 - Tire Rotation + Balance", desc: "Rotation, balance and pressure set on all four.", type: "maintenance", hour: 13, hours: 1.0, mileage: 42100, rate: 95, parts: [] },
  2247. // ── Last week ──
  2248. { w: -1, d: 0, v: 39, tech: 4, title: "Defender 110 - Diff Fluid Service", desc: "Front and rear differential fluid after heavy off-road use.", type: "maintenance", hour: 8, hours: 2.0, mileage: 28300, rate: 120, parts: [{ name: "Diff Fluid 75W-90 (4qt)", partNumber: "LR-DF-7590", quantity: 1, unitPrice: 96, total: 96 }] },
  2249. { w: -1, d: 1, v: 20, tech: 7, title: "Concrete Mixer - Water Pump Repair", desc: "Water system pump leaking at the seal. Rebuilt and tested.", type: "repair", hour: 7, hours: 3.0, mileage: 67500, rate: 140, parts: [{ name: "Water Pump Rebuild Kit", partNumber: "MIX-WPK", quantity: 1, unitPrice: 185, total: 185 }] },
  2250. { w: -1, d: 2, v: 3, tech: 6, title: "F-150 Platinum - Brake Fluid Flush", desc: "Two-year brake fluid service for the municipal fleet.", type: "maintenance", hour: 9, hours: 1.5, mileage: 11800, rate: 110, parts: [{ name: "Brake Fluid DOT4 (1L)", partNumber: "BF-DOT4-1L", quantity: 2, unitPrice: 22, total: 44 }] },
  2251. { w: -1, d: 3, v: 37, tech: 5, title: "Forklift - Battery Watering + Load Test", desc: "Traction battery watered, cells load tested and logged.", type: "maintenance", hour: 8, hours: 2.0, mileage: 4150, rate: 120, parts: [{ name: "Distilled Water (5gal)", partNumber: "FRK-DW-5G", quantity: 1, unitPrice: 18, total: 18 }] },
  2252. { w: -1, d: 4, v: 16, tech: 3, title: "Tacoma - Front Brake Service", desc: "Front pads and rotors replaced, calipers cleaned.", type: "repair", hour: 10, hours: 2.0, mileage: 31000, rate: 110, parts: [{ name: "Front Pads + Rotors", partNumber: "TOY-BPR-TAC", quantity: 1, unitPrice: 285, total: 285 }] },
  2253. // ── Next week ──
  2254. { w: 1, d: 0, v: 7, tech: 3, title: "Tesla Model Y - Brake Fluid Test", desc: "Two-year brake fluid moisture test and flush if needed.", type: "maintenance", hour: 8, hours: 1.5, mileage: 3400, rate: 110, parts: [{ name: "Brake Fluid DOT4 (1L)", partNumber: "BF-DOT4-1L", quantity: 1, unitPrice: 22, total: 22 }] },
  2255. { w: 1, d: 1, v: 29, tech: 2, title: "CAT D6 - Undercarriage Measurement", desc: "Wear measurement on chains, rollers and idlers ahead of the rebuild quote.", type: "inspection", hour: 7, hours: 3.0, mileage: 4300, rate: 150, parts: [] },
  2256. { w: 1, d: 2, v: 14, tech: 6, title: "VW Golf GTI - Haldex + Oil Service", desc: "Oil change and rear diff service ahead of a road trip.", type: "maintenance", hour: 9, hours: 2.0, mileage: 38900, rate: 120, parts: [{ name: "Engine Oil 5W-40 (6qt)", partNumber: "VW-5W40-6Q", quantity: 1, unitPrice: 72, total: 72 }] },
  2257. { w: 1, d: 3, v: 2, tech: 0, title: "F-150 XLT - State Safety Inspection", desc: "Minnesota annual safety inspection with documentation.", type: "inspection", hour: 8, hours: 1.5, mileage: 55400, rate: 95, parts: [] },
  2258. { w: 1, d: 4, v: 27, tech: 2, title: "S780 Combine - Header Inspection", desc: "Pre-season header check: knives, auger, feeder chain.", type: "inspection", hour: 7, hours: 4.0, mileage: 640, rate: 140, parts: [] },
  2259. // ── The week after ──
  2260. { w: 2, d: 0, v: 4, tech: 1, title: "BMW 330i - Front Brake Overhaul", desc: "Accepted quote: OEM pads, rotors and wear sensors.", type: "repair", hour: 8, hours: 3.0, mileage: 56400, rate: 120, parts: [{ name: "OEM Pads + Rotors (front)", partNumber: "BMW-BPR-F34", quantity: 1, unitPrice: 505, total: 505 }] },
  2261. { w: 2, d: 1, v: 18, tech: 9, title: "Volvo FH 640 - Air Suspension Repair", desc: "Accepted quote: replace failed air bag and levelling valve.", type: "repair", hour: 7, hours: 4.0, mileage: 93800, rate: 140, parts: [{ name: "Air Bag + Levelling Valve", partNumber: "VOL-ASK-FH", quantity: 1, unitPrice: 690, total: 690 }] },
  2262. { w: 2, d: 2, v: 11, tech: 9, title: "Jeep Wrangler - Winch Install", desc: "Accepted quote: bumper-mounted winch with isolator and wiring.", type: "repair", hour: 8, hours: 4.0, mileage: 29200, rate: 110, parts: [{ name: "12,000 lb Winch + Wiring Kit", partNumber: "JEEP-WIN-12K", quantity: 1, unitPrice: 880, total: 880 }] },
  2263. { w: 2, d: 3, v: 5, tech: 0, title: "Honda Civic - Clutch Replacement", desc: "Accepted quote: clutch kit, flywheel resurface, hydraulic bleed.", type: "repair", hour: 7, hours: 5.5, mileage: 68200, rate: 95, parts: [{ name: "Clutch Kit", partNumber: "HON-CK-CIV", quantity: 1, unitPrice: 420, total: 420 }] },
  2264. { w: 2, d: 4, v: 34, tech: 8, title: "Kubota KX040 - Hose Reroute", desc: "Reroute the chafing hydraulic hose and fit a protective sleeve.", type: "repair", hour: 9, hours: 2.0, mileage: 950, rate: 130, parts: [{ name: "Hydraulic Hose + Sleeve", partNumber: "KUB-HHS-040", quantity: 1, unitPrice: 145, total: 145 }] },
  2265. ];
  2266. const spreadRecords = [];
  2267. for (const job of spreadJobs) {
  2268. const offset = wd(job.w, job.d);
  2269. const past = offset < 0;
  2270. const labor = [{ description: job.title.split(" - ")[1] || "Workshop labor", hours: job.hours, rate: job.rate, total: Math.round(job.hours * job.rate * 100) / 100 }];
  2271. const endMinutes = job.hour * 60 + Math.round(job.hours * 60);
  2272. const rec = await sr(
  2273. {
  2274. vehicleId: vehicles[job.v].id,
  2275. title: job.title,
  2276. description: job.desc,
  2277. type: job.type,
  2278. status: past ? "completed" : "pending",
  2279. serviceDate: days(offset),
  2280. startDateTime: at(offset, job.hour),
  2281. endDateTime: at(offset, Math.floor(endMinutes / 60), endMinutes % 60),
  2282. mileage: job.mileage,
  2283. technicianId: technicians[job.tech].id,
  2284. techName: technicians[job.tech].name,
  2285. shopName: "Egeland Auto",
  2286. },
  2287. job.parts,
  2288. labor,
  2289. );
  2290. spreadRecords.push(rec);
  2291. }
  2292. console.log(` Created ${spreadRecords.length} jobs across ${new Set(spreadJobs.map(j => j.w)).size} weeks`);
  2293. // -- Invoices & payments --
  2294. // Every completed job gets a number from the same sequence the app would use
  2295. // (workshop.invoiceStartNumber = 1001, prefix "{year}-"), then most of them
  2296. // get paid. Without this the sales page, the revenue report and the past-due
  2297. // list all read zero.
  2298. console.log("\nNumbering invoices and recording payments...");
  2299. const completed = await prisma.serviceRecord.findMany({
  2300. where: { organizationId: ORG_ID, status: "completed" },
  2301. orderBy: { serviceDate: "asc" },
  2302. select: { id: true, serviceDate: true, totalAmount: true, mileage: true },
  2303. });
  2304. const methods = ["card", "transfer", "cash", "card", "transfer"];
  2305. let invoiceSeq = 1001;
  2306. let paidFull = 0, paidPartial = 0, outstanding = 0;
  2307. for (const [i, rec] of completed.entries()) {
  2308. const invoiceDate = rec.serviceDate;
  2309. const dueDate = new Date(invoiceDate);
  2310. dueDate.setDate(dueDate.getDate() + 14);
  2311. await prisma.serviceRecord.update({
  2312. where: { id: rec.id },
  2313. data: {
  2314. invoiceNumber: `${YEAR}-${invoiceSeq++}`,
  2315. invoiceDate,
  2316. invoiceDueDate: dueDate,
  2317. // The statement as well as the numbers: the designer's warranty panel
  2318. // and the work order page read warrantyStatus first.
  2319. ...warrantyColumns(invoiceDate),
  2320. },
  2321. });
  2322. // 7 in 10 settled, 1 part-paid, 2 still open - enough of a spread that the
  2323. // past-due list and the outstanding total are both non-empty.
  2324. const bucket = i % 10;
  2325. if (bucket < 7) {
  2326. const paidOn = new Date(invoiceDate);
  2327. paidOn.setDate(paidOn.getDate() + (i % 9));
  2328. await prisma.payment.create({ data: { serviceRecordId: rec.id, amount: rec.totalAmount, date: paidOn > NOW ? NOW : paidOn, method: methods[i % methods.length], note: "Paid in full" } });
  2329. paidFull++;
  2330. } else if (bucket === 7) {
  2331. const paidOn = new Date(invoiceDate);
  2332. paidOn.setDate(paidOn.getDate() + 2);
  2333. await prisma.payment.create({ data: { serviceRecordId: rec.id, amount: Math.round(rec.totalAmount * 0.5 * 100) / 100, date: paidOn > NOW ? NOW : paidOn, method: "transfer", note: "Deposit - balance on collection" } });
  2334. paidPartial++;
  2335. } else {
  2336. outstanding++;
  2337. }
  2338. }
  2339. console.log(` ${completed.length} invoices numbered - ${paidFull} paid, ${paidPartial} part-paid, ${outstanding} outstanding`);
  2340. // -- Stock movements --
  2341. // Inventory quantities without a history read as if they were typed in once.
  2342. // These are the consumption and restock entries behind the current counts.
  2343. console.log("\nCreating stock movements...");
  2344. const movementPlan: { part: number; entries: { delta: number; reason: string; day: number; note?: string }[] }[] = [
  2345. { part: 0, entries: [{ delta: 24, reason: "manual_adjustment", day: -46, note: "Pallet delivery from AutoZone Commercial" }, { delta: -4, reason: "service_record", day: -21 }, { delta: -2, reason: "service_record", day: -12 }, { delta: -3, reason: "service_record", day: -4 }] },
  2346. { part: 1, entries: [{ delta: 20, reason: "manual_adjustment", day: -40, note: "Restock" }, { delta: -2, reason: "service_record", day: -18 }, { delta: -1, reason: "service_record", day: -6 }] },
  2347. { part: 4, entries: [{ delta: 18, reason: "manual_adjustment", day: -52, note: "Quarterly brake stock order" }, { delta: -2, reason: "service_record", day: -25 }, { delta: -1, reason: "quote_conversion", day: -9 }, { delta: -1, reason: "service_record", day: -2 }] },
  2348. { part: 6, entries: [{ delta: 36, reason: "manual_adjustment", day: -60, note: "Bulk filter order" }, { delta: -3, reason: "service_record", day: -30 }, { delta: -2, reason: "service_record", day: -11 }, { delta: -1, reason: "service_record", day: -1 }] },
  2349. { part: 10, entries: [{ delta: 8, reason: "manual_adjustment", day: -35, note: "Interstate Batteries delivery" }, { delta: -1, reason: "service_record", day: -16 }, { delta: -1, reason: "service_record", day: -3 }] },
  2350. { part: 21, entries: [{ delta: 4, reason: "manual_adjustment", day: -50, note: "Bosch order" }, { delta: -2, reason: "service_record", day: -28 }, { delta: -1, reason: "service_record", day: -7, note: "Kenworth DPF fault" }] },
  2351. { part: 15, entries: [{ delta: 12, reason: "manual_adjustment", day: -44 }, { delta: -1, reason: "service_record", day: -20 }, { delta: -1, reason: "service_record", day: -5 }] },
  2352. { part: 24, entries: [{ delta: 16, reason: "manual_adjustment", day: -38 }, { delta: -2, reason: "service_record", day: -19 }, { delta: -2, reason: "bulk_markup", day: -8, note: "Counter sale" }] },
  2353. // Fractional deltas: the ledger tracks bulk refrigerant by the pound.
  2354. { part: 26, entries: [{ delta: 15, reason: "manual_adjustment", day: -33, note: "Bulk cylinder delivery" }, { delta: -1.4, reason: "service_record", day: -15, note: "A/C recharge, F-150" }, { delta: -1.2, reason: "service_record", day: -6, note: "A/C recharge, Camry" }] },
  2355. ];
  2356. let movementCount = 0;
  2357. for (const plan of movementPlan) {
  2358. const part = inventoryParts[plan.part];
  2359. // Walk backwards from today's quantity so the running balance ends on it.
  2360. const total = plan.entries.reduce((sum, e) => sum + e.delta, 0);
  2361. let balance = part.quantity - total;
  2362. for (const entry of plan.entries) {
  2363. balance += entry.delta;
  2364. await prisma.stockMovement.create({
  2365. data: {
  2366. inventoryPartId: part.id,
  2367. delta: entry.delta,
  2368. quantityAfter: balance,
  2369. reason: entry.reason,
  2370. note: entry.note ?? null,
  2371. serviceRecordLabel: entry.reason === "service_record" ? "Workshop job" : null,
  2372. userId: USER_ID,
  2373. organizationId: ORG_ID,
  2374. createdAt: at(entry.day, 9, 30),
  2375. },
  2376. });
  2377. movementCount++;
  2378. }
  2379. }
  2380. console.log(` Created ${movementCount} stock movements across ${movementPlan.length} parts`);
  2381. // -- Inspection templates & inspections --
  2382. // The templates page syncs the built-in presets on first load, but nothing
  2383. // creates inspections, so the feature reads as unused. These build the same
  2384. // templates the library would install, then run vehicles through them.
  2385. console.log("\nCreating inspection templates...");
  2386. // The library's own checklists carry its packageId, which is how
  2387. // syncPresetLibrary() knows they are installed and does not add a second
  2388. // copy; it installs only the presets not seeded here when the templates page
  2389. // first loads. The periodic checklist is the workshop's own (no packageId).
  2390. const toTemplateCreate = (preset: SeedTemplate, isDefault: boolean) => ({
  2391. name: preset.name,
  2392. description: preset.description,
  2393. isDefault,
  2394. country: preset.country,
  2395. standard: preset.standard,
  2396. severityScale: preset.severityScale,
  2397. packageId: preset.packageId ?? null,
  2398. packageVersion: preset.packageVersion ?? null,
  2399. packageSource: preset.packageSource ?? null,
  2400. organizationId: ORG_ID,
  2401. sections: {
  2402. create: preset.sections.map((section, sIdx) => ({
  2403. name: section.name,
  2404. description: section.description || null,
  2405. code: section.code || null,
  2406. sortOrder: sIdx,
  2407. items: {
  2408. create: section.items.map((item, iIdx) => ({
  2409. name: item.name,
  2410. description: item.description || null,
  2411. code: item.code || null,
  2412. sortOrder: iIdx,
  2413. inputType: item.inputType ?? "condition",
  2414. unit: item.unit || null,
  2415. minValue: item.minValue ?? null,
  2416. maxValue: item.maxValue ?? null,
  2417. choices: item.choices ?? [],
  2418. required: item.required ?? false,
  2419. photoRequired: item.photoRequired ?? false,
  2420. allowNotApplicable: item.allowNotApplicable ?? true,
  2421. defaultSeverity: item.defaultSeverity ?? null,
  2422. defectSuggestions: [],
  2423. })),
  2424. },
  2425. })),
  2426. },
  2427. });
  2428. const createTemplate = (preset: SeedTemplate) =>
  2429. prisma.inspectionTemplate.create({
  2430. // The general checklist is the default, as a first library install makes it.
  2431. data: toTemplateCreate(preset, preset.packageId === PRESET_MULTIPOINT),
  2432. include: { sections: { include: { items: { orderBy: { sortOrder: "asc" } } }, orderBy: { sortOrder: "asc" } } },
  2433. });
  2434. const templates: Awaited<ReturnType<typeof createTemplate>>[] = [];
  2435. for (const preset of SEED_TEMPLATES) templates.push(await createTemplate(preset));
  2436. console.log(` Created ${templates.length} templates`);
  2437. console.log("\nCreating inspections...");
  2438. type TemplateWithSections = (typeof templates)[number];
  2439. /** A graded finding, on the check with this name; `value` is its reading. */
  2440. type Defect = { item: string; condition: string; notes: string; value?: number };
  2441. /**
  2442. * Copies a template's checks onto an inspection. Everything grades `pass`
  2443. * unless it is listed in `defects`, and an in-progress inspection leaves the
  2444. * checks past `graded` untouched - which is what the completion blockers and
  2445. * the progress bar in the UI read. `answers` fills text and choice checks
  2446. * by name (a choice is stored in textValue, as the app's select saves it).
  2447. */
  2448. const buildInspection = async (opts: {
  2449. template: TemplateWithSections;
  2450. vehicleIdx: number;
  2451. techIdx: number;
  2452. dayOffset: number;
  2453. hour: number;
  2454. mileage: number;
  2455. status: "in_progress" | "completed";
  2456. defects?: Defect[];
  2457. answers?: Record<string, string>;
  2458. graded?: number;
  2459. category?: string;
  2460. certificate?: string;
  2461. notes?: string;
  2462. nextTestMonths?: number;
  2463. /** Minutes from start to completion; two hours unless said. */
  2464. durationMinutes?: number;
  2465. /** An exact start, instead of dayOffset and hour. */
  2466. startAt?: Date;
  2467. }) => {
  2468. const flat = opts.template.sections.flatMap((s) => s.items.map((item) => ({ item, section: s })));
  2469. const indexOf = (name: string) => {
  2470. const idx = flat.findIndex(({ item }) => item.name === name);
  2471. // A renamed check in the library must fail the seed loudly rather than
  2472. // move a defect onto some other line.
  2473. if (idx < 0) throw new Error(`No check named "${name}" on ${opts.template.name}`);
  2474. return idx;
  2475. };
  2476. const defects = new Map((opts.defects ?? []).map((d) => [indexOf(d.item), d]));
  2477. const answers = new Map(Object.entries(opts.answers ?? {}).map(([name, value]) => [indexOf(name), value]));
  2478. const graded = opts.graded ?? flat.length;
  2479. const startAt = opts.startAt ?? at(opts.dayOffset, opts.hour);
  2480. const completedAt =
  2481. opts.status === "completed"
  2482. ? new Date(startAt.getTime() + (opts.durationMinutes ?? 120) * 60_000)
  2483. : null;
  2484. const nextTest = opts.nextTestMonths ? months(opts.nextTestMonths) : null;
  2485. return prisma.inspection.create({
  2486. data: {
  2487. status: opts.status,
  2488. mileage: opts.mileage,
  2489. notes: opts.notes ?? null,
  2490. startDateTime: startAt,
  2491. endDateTime: completedAt,
  2492. completedAt,
  2493. createdAt: startAt,
  2494. publicToken: randomBytes(16).toString("hex"),
  2495. vehicleId: vehicles[opts.vehicleIdx].id,
  2496. templateId: opts.template.id,
  2497. technicianId: technicians[opts.techIdx].id,
  2498. organizationId: ORG_ID,
  2499. severityScale: opts.template.severityScale,
  2500. country: opts.template.country,
  2501. vehicleCategory: opts.category ?? null,
  2502. nextTestDue: nextTest,
  2503. certificateNumber: opts.certificate ?? null,
  2504. inspectorName: opts.status === "completed" ? technicians[opts.techIdx].name : null,
  2505. testLocation: opts.status === "completed" ? "Egeland Auto, Denver CO" : null,
  2506. items: {
  2507. create: flat.map(({ item, section }, idx) => {
  2508. const defect = defects.get(idx);
  2509. const inspected = idx < graded;
  2510. const condition = defect ? defect.condition : inspected ? "pass" : "not_inspected";
  2511. const measured =
  2512. item.inputType === "measurement" && inspected
  2513. ? defect
  2514. ? (defect.value ?? (item.minValue ?? 0) * 0.6)
  2515. : item.minValue != null && item.maxValue != null
  2516. ? Math.round(((item.minValue + item.maxValue) / 2) * 10) / 10
  2517. : (item.minValue ?? item.maxValue ?? 0)
  2518. : null;
  2519. const answer = answers.get(idx);
  2520. const textValue =
  2521. !inspected ? null
  2522. : answer !== undefined ? answer
  2523. : item.inputType === "text" ? "No remarks"
  2524. : item.inputType === "choice" ? (item.choices[0] ?? null)
  2525. : null;
  2526. return {
  2527. name: item.name,
  2528. section: section.name,
  2529. sortOrder: idx,
  2530. condition,
  2531. notes: defect?.notes ?? null,
  2532. description: item.description,
  2533. code: item.code,
  2534. sectionCode: section.code,
  2535. inputType: item.inputType,
  2536. unit: item.unit,
  2537. minValue: item.minValue,
  2538. maxValue: item.maxValue,
  2539. choices: item.choices,
  2540. required: item.required,
  2541. photoRequired: item.photoRequired,
  2542. allowNotApplicable: item.allowNotApplicable,
  2543. defaultSeverity: item.defaultSeverity,
  2544. measuredValue: measured,
  2545. textValue,
  2546. };
  2547. }),
  2548. },
  2549. },
  2550. include: { items: { orderBy: { sortOrder: "asc" } } },
  2551. });
  2552. };
  2553. const templateByPackage = (packageId: string) => {
  2554. const found = templates.find((t) => t.packageId === packageId);
  2555. if (!found) throw new Error(`Seed template ${packageId} missing`);
  2556. return found;
  2557. };
  2558. const multipoint = templateByPackage(PRESET_MULTIPOINT);
  2559. const prePurchase = templateByPackage(PRESET_PRE_PURCHASE);
  2560. const vehicleIntake = templateByPackage(PRESET_VEHICLE_INTAKE);
  2561. const roadworthiness = templates.find((t) => t.standard === "eu-2014-45") ?? multipoint;
  2562. const inspections = [
  2563. // Completed, clean
  2564. await buildInspection({ template: multipoint, vehicleIdx: 0, techIdx: 0, dayOffset: wd(-2, 1), hour: 9, mileage: 17900, status: "completed", category: "M1", notes: "Everything within spec. Customer wants a call before the next service." }),
  2565. await buildInspection({ template: multipoint, vehicleIdx: 6, techIdx: 3, dayOffset: wd(-1, 2), hour: 11, mileage: 8100, status: "completed", category: "M1" }),
  2566. // Completed with defects
  2567. await buildInspection({ template: multipoint, vehicleIdx: 5, techIdx: 0, dayOffset: wd(-1, 0), hour: 8, mileage: 67500, status: "completed", category: "M1", notes: "Two items need booking in. Quote requested by the customer.", defects: [{ item: "CV joints and boots", condition: "fail", notes: "CV boot torn, grease thrown onto the suspension arm." }, { item: "Front pads", condition: "attention", value: 3, notes: "Front pads at 3mm - plan replacement within 5,000 miles." }] }),
  2568. await buildInspection({ template: multipoint, vehicleIdx: 8, techIdx: 2, dayOffset: wd(-1, 3), hour: 13, mileage: 98200, status: "completed", category: "N2", defects: [{ item: "Battery voltage", condition: "attention", value: 12.1, notes: "Rests at 12.1 V and load tests at 78% of rated CCA." }] }),
  2569. // Roadworthiness tests with certificates
  2570. await buildInspection({ template: roadworthiness, vehicleIdx: 18, techIdx: 2, dayOffset: wd(-2, 3), hour: 7, mileage: 92400, status: "completed", category: "N3", certificate: `PTI-${YEAR}-0418`, nextTestMonths: 12, notes: "Passed after the marker lights and wipers were replaced during the test.", defects: [{ item: "Position, side marker and daytime running lamps", condition: "attention", notes: "Nearside marker lens cracked - replaced during the test." }] }),
  2571. await buildInspection({ template: roadworthiness, vehicleIdx: 4, techIdx: 1, dayOffset: wd(-1, 4), hour: 9, mileage: 56100, status: "completed", category: "M1", certificate: `PTI-${YEAR}-0431`, nextTestMonths: 24 }),
  2572. await buildInspection({ template: roadworthiness, vehicleIdx: 9, techIdx: 1, dayOffset: wd(0, 1), hour: 8, mileage: 35700, status: "completed", category: "N1", certificate: `PTI-${YEAR}-0447`, nextTestMonths: 12, notes: "Retest after the turbo replacement. Passed.", defects: [{ item: "Mechanical condition of the steering", condition: "attention", notes: "Slight play in the offside track rod end - monitor." }] }),
  2573. // Failed test - dangerous defect, vehicle held
  2574. await buildInspection({ template: roadworthiness, vehicleIdx: 19, techIdx: 2, dayOffset: wd(0, 0), hour: 10, mileage: 156200, status: "completed", category: "N3", certificate: `PTI-${YEAR}-0452`, notes: "Fails on the dump body cracking. Vehicle not to be loaded until welded.", defects: [{ item: "Cab and bodywork condition", condition: "dangerous", notes: "Structural cracks in the dump body floor near the rear hinge." }, { item: "Brake linings and pads", condition: "fail", value: 2.1, notes: "Offside rear brake lining below the minimum thickness." }] }),
  2575. // In progress on the board right now
  2576. await buildInspection({ template: multipoint, vehicleIdx: 13, techIdx: 10, dayOffset: 0, hour: 9, mileage: 13000, status: "in_progress", graded: 6, category: "M1", notes: "Pre-track inspection for the customer's HPDE weekend." }),
  2577. await buildInspection({ template: roadworthiness, vehicleIdx: 2, techIdx: 6, dayOffset: 0, hour: 13, mileage: 55300, status: "in_progress", graded: 14, category: "N1" }),
  2578. await buildInspection({ template: prePurchase, vehicleIdx: 40, techIdx: 0, dayOffset: wd(0, 2), hour: 10, mileage: 34900, status: "in_progress", graded: 4, category: "M1", notes: "Buyer is paying for the inspection. Report goes to them, not the seller." }),
  2579. // Vehicle check-in (the library's walk-round at drop-off), with the body
  2580. // marked on the condition map further down. [11] was done when the
  2581. // Sprinter came in for its turbo and is linked to that job; [12] is at the
  2582. // desk right now.
  2583. await buildInspection({ template: vehicleIntake, vehicleIdx: 9, techIdx: 1, dayOffset: -18, hour: 7, startAt: at(-18, 7, 40), durationMinutes: 15, mileage: 35400, status: "completed", category: "N1", notes: "Walked round with Kevin at drop-off. He signed the sheet.", answers: { "Fuel level": "1/4", "Keys handed over": "One key with fob", "Warning lights on arrival": "Check engine (P0299) and glow plug lamp", "Personal belongings left in the vehicle": "Catering trays in the load area, customer aware" }, defects: [{ item: "Body condition", condition: "attention", notes: "Three existing marks recorded with the customer, see the drawing." }, { item: "Glass condition", condition: "attention", notes: "Crack in the windshield from the lower edge, outside the wiper sweep." }] }),
  2584. await buildInspection({ template: vehicleIntake, vehicleIdx: 11, techIdx: 6, dayOffset: 0, hour: 0, startAt: minutesAgo(35), mileage: 28900, status: "in_progress", graded: 5, category: "M1", answers: { "Fuel level": "3/4", "Keys handed over": "Two keys", "Warning lights on arrival": "None", "Personal belongings left in the vehicle": "Child seat, rear left" }, defects: [{ item: "Body condition", condition: "attention", notes: "Two new marks, one known from the last visit." }] }),
  2585. ];
  2586. console.log(` Created ${inspections.length} inspections`);
  2587. // A customer asking for a quote off the back of a failed check.
  2588. await prisma.inspectionQuoteRequest.create({
  2589. data: {
  2590. inspectionId: inspections[2].id,
  2591. organizationId: ORG_ID,
  2592. status: "pending",
  2593. message: "Please quote both items. I can leave the car with you next week.",
  2594. selectedItemIds: [],
  2595. createdAt: hoursAgo(30),
  2596. },
  2597. });
  2598. await prisma.inspectionQuoteRequest.create({
  2599. data: {
  2600. inspectionId: inspections[7].id,
  2601. organizationId: ORG_ID,
  2602. status: "quoted",
  2603. message: "Send the weld repair cost to ops@ridgelineexcavation.example.com.",
  2604. selectedItemIds: [],
  2605. createdAt: hoursAgo(52),
  2606. },
  2607. });
  2608. // The quote that request was answered with, raised from the failed test so
  2609. // the inspection and the quote link to each other.
  2610. const weldParts = [
  2611. { name: "Steel plate 3/8\" AR400, 4x8 ft", partNumber: "STL-AR400-38", quantity: 1, unitPrice: 690, total: 690 },
  2612. { name: "Welding wire and gas", partNumber: "WLD-CONS", quantity: 1, unitPrice: 85, total: 85 },
  2613. { name: "Rear brake lining kit (axle set)", partNumber: "MACK-BL-R", quantity: 1, unitPrice: 245, total: 245 },
  2614. ];
  2615. const weldLabor = [
  2616. { description: "Cut out cracked floor section, weld in new plate and reinforce hinge area", hours: 9, rate: 140, total: 1260 },
  2617. { description: "Rear brake reline, adjust and test", hours: 2.5, rate: 140, total: 350 },
  2618. ];
  2619. const weldSubtotal = [...weldParts, ...weldLabor].reduce((sum, line) => sum + line.total, 0);
  2620. const inspectionQuote = await prisma.quote.create({
  2621. data: {
  2622. quoteNumber: `Q-${YEAR}-016`,
  2623. title: "Mack Granite - Dump Body Floor Weld Repair",
  2624. status: "sent",
  2625. validUntil: days(21),
  2626. notes: "Raised from the failed periodic inspection. The truck must not be loaded until the floor is welded.",
  2627. subtotal: weldSubtotal,
  2628. taxRate: 8,
  2629. taxAmount: Math.round(weldSubtotal * 0.08 * 100) / 100,
  2630. totalAmount: Math.round(weldSubtotal * 1.08 * 100) / 100,
  2631. userId: USER_ID,
  2632. organizationId: ORG_ID,
  2633. customerId: vehicles[19].customerId,
  2634. vehicleId: vehicles[19].id,
  2635. inspectionId: inspections[7].id,
  2636. createdAt: hoursAgo(49),
  2637. ...warrantyColumns(null),
  2638. partItems: { create: weldParts },
  2639. laborItems: { create: weldLabor },
  2640. },
  2641. });
  2642. quotes.push(inspectionQuote);
  2643. // The Sprinter's check-in is its turbo job's drop-off, done on the
  2644. // inspection form. The job links to it, and prints its condition map.
  2645. const sprinterCheckIn = inspections[11];
  2646. const rubiconCheckIn = inspections[12];
  2647. await prisma.serviceRecord.update({
  2648. where: { id: serviceRecords[12].id },
  2649. data: { inspectionId: sprinterCheckIn.id, conditionMapOnInvoice: true },
  2650. });
  2651. // -- Condition map --
  2652. // Marks on the five-view drawing (src/features/condition-map). A mark is
  2653. // drawn on a job's drop-off (serviceRecordId), on an inspection's map check
  2654. // (inspectionId + inspectionItemId), or it is an older one still open from
  2655. // an earlier visit, which the map shows in grey for the technician to
  2656. // confirm or clear. Positions are fractions of the view's box and each one
  2657. // sits inside the named panel of that vehicle's drawing.
  2658. console.log("\nCreating condition map marks...");
  2659. const mapItemOf = (inspection: (typeof inspections)[number]) => {
  2660. const item = inspection.items.find((i) => i.inputType === "condition_map");
  2661. if (!item) throw new Error("Inspection has no condition map check");
  2662. return item;
  2663. };
  2664. type SeedMark = {
  2665. vehicleIdx: number;
  2666. view: "top" | "left" | "right" | "front" | "rear";
  2667. panel: string;
  2668. x: number;
  2669. y: number;
  2670. kind: "dent" | "scratch" | "chip" | "crack" | "rust" | "paint" | "previous_repair" | "missing";
  2671. severity?: "minor" | "major";
  2672. note: string;
  2673. recordedAt: Date;
  2674. serviceRecordId?: string;
  2675. inspection?: (typeof inspections)[number];
  2676. resolvedAt?: Date;
  2677. };
  2678. const marks: SeedMark[] = [
  2679. // Drop-off, Audi A4 timing belt job
  2680. { vehicleIdx: 10, view: "right", panel: "right_rear_door", x: 0.61, y: 0.55, kind: "scratch", note: "Light scratch across the rear door, below the handle.", recordedAt: at(MONDAY_OFFSET, 7, 20), serviceRecordId: boardServiceRecords[1].id },
  2681. { vehicleIdx: 10, view: "front", panel: "front_bumper", x: 0.45, y: 0.62, kind: "chip", note: "Stone chips on the front bumper.", recordedAt: at(MONDAY_OFFSET, 7, 20), serviceRecordId: boardServiceRecords[1].id },
  2682. // Drop-off, Honda Civic A/C job
  2683. { vehicleIdx: 5, view: "left", panel: "left_front_fender", x: 0.25, y: 0.56, kind: "dent", severity: "major", note: "Fist-sized dent above the wheel arch. Customer says it was there when she bought it.", recordedAt: at(MONDAY_OFFSET, 7, 50), serviceRecordId: boardServiceRecords[0].id },
  2684. // Drop-off, F-150 XLT wheel bearing job
  2685. { vehicleIdx: 2, view: "left", panel: "bed_side_left", x: 0.8, y: 0.54, kind: "scratch", severity: "major", note: "Deep scratches along the bed side from a load of lumber.", recordedAt: at(MONDAY_OFFSET + 1, 8, 40), serviceRecordId: boardServiceRecords[12].id },
  2686. { vehicleIdx: 2, view: "rear", panel: "tailgate", x: 0.5, y: 0.48, kind: "dent", note: "Small dent in the tailgate, center.", recordedAt: at(MONDAY_OFFSET + 1, 8, 40), serviceRecordId: boardServiceRecords[12].id },
  2687. // Sprinter check-in, drawn on the inspection's map check
  2688. { vehicleIdx: 9, view: "left", panel: "left_sliding_door", x: 0.57, y: 0.56, kind: "scratch", note: "Scrape along the lower edge of the sliding door.", recordedAt: at(-18, 7, 45), inspection: sprinterCheckIn },
  2689. { vehicleIdx: 9, view: "rear", panel: "rear_bumper", x: 0.5, y: 0.67, kind: "dent", severity: "major", note: "Step bumper pushed in. Reverse sensors still work.", recordedAt: at(-18, 7, 46), inspection: sprinterCheckIn },
  2690. { vehicleIdx: 9, view: "front", panel: "windshield", x: 0.56, y: 0.31, kind: "crack", severity: "major", note: "Crack running up from the lower edge, about 8 inches.", recordedAt: at(-18, 7, 48), inspection: sprinterCheckIn },
  2691. // Jeep Rubicon check-in in progress at the desk
  2692. { vehicleIdx: 11, view: "front", panel: "windshield", x: 0.56, y: 0.29, kind: "chip", note: "Stone chip, passenger side, outside the wiper sweep.", recordedAt: minutesAgo(30), inspection: rubiconCheckIn },
  2693. { vehicleIdx: 11, view: "left", panel: "left_front_door", x: 0.44, y: 0.52, kind: "scratch", severity: "major", note: "Key scratch along the belt line of the driver's door.", recordedAt: minutesAgo(28), inspection: rubiconCheckIn },
  2694. // Still open from earlier visits: grey on the next job's map
  2695. { vehicleIdx: 11, view: "rear", panel: "rear_bumper", x: 0.5, y: 0.63, kind: "rust", note: "Surface rust on the rear bumper step. Customer will treat it himself.", recordedAt: serviceRecords[11].serviceDate, serviceRecordId: serviceRecords[11].id },
  2696. { vehicleIdx: 0, view: "right", panel: "right_rear_door", x: 0.6, y: 0.55, kind: "dent", note: "Door ding, customer aware.", recordedAt: serviceRecords[0].serviceDate, serviceRecordId: serviceRecords[0].id },
  2697. { vehicleIdx: 1, view: "rear", panel: "tailgate", x: 0.47, y: 0.47, kind: "scratch", note: "Scratches around the tailgate handle.", recordedAt: serviceRecords[18].serviceDate, serviceRecordId: serviceRecords[18].id },
  2698. // Resolved: the body shop refinished it after the visit it was found on
  2699. { vehicleIdx: 4, view: "front", panel: "front_bumper", x: 0.52, y: 0.62, kind: "paint", note: "Scuffed paint on the lower valance.", recordedAt: serviceRecords[1].serviceDate, serviceRecordId: serviceRecords[1].id, resolvedAt: new Date(Math.min(serviceRecords[1].serviceDate.getTime() + 21 * 86_400_000, NOW.getTime())) },
  2700. ];
  2701. await Promise.all(
  2702. marks.map((m) =>
  2703. prisma.conditionMark.create({
  2704. data: {
  2705. organizationId: ORG_ID,
  2706. vehicleId: vehicles[m.vehicleIdx].id,
  2707. serviceRecordId: m.serviceRecordId ?? null,
  2708. inspectionId: m.inspection?.id ?? null,
  2709. inspectionItemId: m.inspection ? mapItemOf(m.inspection).id : null,
  2710. bodyType: bodyTypeOf(m.vehicleIdx),
  2711. view: m.view,
  2712. panel: m.panel,
  2713. x: m.x,
  2714. y: m.y,
  2715. kind: m.kind,
  2716. severity: m.severity ?? "minor",
  2717. note: m.note,
  2718. recordedAt: m.recordedAt,
  2719. recordedById: USER_ID,
  2720. createdAt: m.recordedAt,
  2721. resolvedAt: m.resolvedAt ?? null,
  2722. resolvedById: m.resolvedAt ? USER_ID : null,
  2723. },
  2724. }),
  2725. ),
  2726. );
  2727. console.log(` Created ${marks.length} condition marks (${marks.filter((m) => m.resolvedAt).length} resolved)`);
  2728. // Drop-off photos and a check-in photo, from the vehicle's own cached
  2729. // stock photo. Skipped for any vehicle whose photo never downloaded.
  2730. const dropoffPhotos: { recordId: string; vehicleFile: string; name: string; slot: string; at: Date }[] = [
  2731. { recordId: boardServiceRecords[1].id, vehicleFile: "audi-a4.jpg", name: "seed-dropoff-audi-a4-front.jpg", slot: "front", at: at(MONDAY_OFFSET, 7, 18) },
  2732. { recordId: boardServiceRecords[0].id, vehicleFile: "honda-civic.jpg", name: "seed-dropoff-honda-civic-front.jpg", slot: "front", at: at(MONDAY_OFFSET, 7, 48) },
  2733. { recordId: boardServiceRecords[12].id, vehicleFile: "ford-f150-grey.jpg", name: "seed-dropoff-f150-xlt-front.jpg", slot: "front", at: at(MONDAY_OFFSET + 1, 8, 38) },
  2734. ];
  2735. let photoCount = 0;
  2736. for (const photo of dropoffPhotos) {
  2737. const file = seedPhoto(photo.vehicleFile, "services", photo.name);
  2738. if (!file) continue;
  2739. await prisma.serviceAttachment.create({
  2740. data: { ...file, category: "dropoff", description: photo.slot, includeInInvoice: false, serviceRecordId: photo.recordId, createdAt: photo.at },
  2741. });
  2742. photoCount++;
  2743. }
  2744. const checkInPhoto = seedPhoto("mercedes-sprinter.jpg", "inspections", "seed-checkin-sprinter-front.jpg");
  2745. if (checkInPhoto) {
  2746. await prisma.inspectionAttachment.create({
  2747. data: { ...checkInPhoto, category: "image", description: "Front on arrival", includeInReport: true, inspectionId: sprinterCheckIn.id, createdAt: at(-18, 7, 42) },
  2748. });
  2749. photoCount++;
  2750. }
  2751. console.log(` Attached ${photoCount} drop-off and check-in photo(s)`);
  2752. // -- Work bays --
  2753. // The bays the board can plan by, replacing the old "Workshop bay" custom
  2754. // field, which is no longer seeded. A few of this week's jobs are in one.
  2755. console.log("\nCreating work bays...");
  2756. const bays = await Promise.all(
  2757. [
  2758. { name: "Bay 1", color: "#3b82f6" },
  2759. { name: "Bay 2", color: "#22c55e" },
  2760. { name: "Bay 3", color: "#f59e0b" },
  2761. { name: "Heavy bay 1", color: "#ef4444" },
  2762. { name: "Heavy bay 2", color: "#8b5cf6" },
  2763. { name: "Alignment rack", color: "#06b6d4" },
  2764. ].map((bay, sortOrder) => prisma.workBay.create({ data: { ...bay, sortOrder, organizationId: ORG_ID } })),
  2765. );
  2766. const bayAssignments: [string, number][] = [
  2767. [boardServiceRecords[1].id, 0], // Audi timing belt, Monday
  2768. [boardServiceRecords[0].id, 1], // Honda A/C, Monday
  2769. [boardServiceRecords[6].id, 3], // CAT 745 rebuild, Monday to Wednesday
  2770. [boardServiceRecords[13].id, 4], // Concrete mixer, Monday
  2771. [boardServiceRecords[3].id, 0], // Silverado 60K, Tuesday
  2772. [boardServiceRecords[2].id, 1], // Tesla Model Y, Tuesday
  2773. [serviceRecords[12].id, 2], // Sprinter turbo, Tuesday
  2774. [boardServiceRecords[26].id, 5], // Ram alignment, Friday
  2775. ];
  2776. await Promise.all(bayAssignments.map(([id, bay]) => prisma.serviceRecord.update({ where: { id }, data: { workBayId: bays[bay].id } })));
  2777. console.log(` Created ${bays.length} bays, ${bayAssignments.length} jobs placed`);
  2778. // -- Work order statuses --
  2779. // The workshop's own labels under the three fixed stages
  2780. // (src/features/work-order-statuses). A label must sit under the stage of
  2781. // the job's own status, so each is set on a job already at that stage.
  2782. console.log("\nCreating work order statuses...");
  2783. const statusDefs = [
  2784. { name: "Awaiting approval", stage: "pending", color: "amber" },
  2785. { name: "Booked in", stage: "pending", color: "blue" },
  2786. { name: "Road test", stage: "in-progress", color: "sky" },
  2787. { name: "Quality check", stage: "in-progress", color: "violet" },
  2788. { name: "Ready for pickup", stage: "completed", color: "emerald", notifyCustomer: true, messageTemplate: "Hi {customer_name}, your {vehicle} is ready for pickup. {company_name}" },
  2789. ];
  2790. const statusRows = await Promise.all(
  2791. statusDefs.map((s, i) =>
  2792. prisma.workOrderStatus.create({ data: { ...s, sortOrder: i, organizationId: ORG_ID } }),
  2793. ),
  2794. );
  2795. const statusByName = (name: string) => statusRows.find((s) => s.name === name)!.id;
  2796. const customStatuses: [string, string, Date][] = [
  2797. // Metro City will not sign off the exhaust job without a purchase order.
  2798. [boardServiceRecords[7].id, "Awaiting approval", hoursAgo(26)],
  2799. [boardServiceRecords[3].id, "Booked in", hoursAgo(50)],
  2800. [serviceRecords[12].id, "Road test", hoursAgo(3)],
  2801. [serviceRecords[10].id, "Quality check", hoursAgo(5)],
  2802. [spreadRecords[14].id, "Ready for pickup", spreadRecords[14].endDateTime ?? hoursAgo(20)],
  2803. ];
  2804. await Promise.all(
  2805. customStatuses.map(([id, name, since]) =>
  2806. prisma.serviceRecord.update({ where: { id }, data: { customStatusId: statusByName(name), customStatusSince: since } }),
  2807. ),
  2808. );
  2809. console.log(` Created ${statusRows.length} statuses, ${customStatuses.length} in use`);
  2810. // -- Concerns --
  2811. // What the customer reported, what was found and what was done about it,
  2812. // on the jobs being worked this week.
  2813. console.log("\nCreating customer concerns...");
  2814. const concerns: { recordId: string; description: string; cause?: string; correction?: string; confirmation?: string; confirmedAt?: Date }[] = [
  2815. { recordId: boardServiceRecords[0].id, description: "A/C blows warm air, at idle and on the highway.", cause: "Compressor clutch coil open circuit, so the clutch never engages.", correction: "Replace the compressor, evacuate and recharge with 1.4 lb of R-1234yf." },
  2816. { recordId: boardServiceRecords[1].id, description: "Timing belt and water pump due at 60,000 miles.", cause: "Scheduled maintenance interval.", correction: "Timing belt kit with water pump and thermostat, coolant flushed and bled." },
  2817. { recordId: boardServiceRecords[2].id, description: "Clunk from the front left over bumps and driveway ramps." },
  2818. { recordId: boardServiceRecords[12].id, description: "Humming from the front that rises with speed.", cause: "Front left hub bearing has play on the lift.", correction: "Replace the front left hub bearing assembly (on order)." },
  2819. { recordId: serviceRecords[12].id, description: "Black smoke under boost and loss of power on the highway.", cause: "Turbo shaft play well past spec; the compressor wheel had contacted the housing.", correction: "Replaced the turbocharger and oil feed line, changed oil and filter.", confirmation: "Road tested 15 miles under load. No smoke, boost within spec.", confirmedAt: hoursAgo(3) },
  2820. { recordId: serviceRecords[12].id, description: "Check engine light on.", cause: "P0299 underboost, set by the failed turbo.", correction: "Codes cleared after the repair; none returned on the road test.", confirmation: "Scan tool shows no stored or pending codes.", confirmedAt: hoursAgo(3) },
  2821. { recordId: unassignedRecords[11].id, description: "Sleeper A/C blows warm.", cause: "Compressor seized and sent debris through the condenser.", correction: "Replace compressor, condenser and receiver-drier, flush the lines." },
  2822. ];
  2823. const concernOrder = new Map<string, number>();
  2824. await Promise.all(
  2825. concerns.map((c) => {
  2826. const sortOrder = concernOrder.get(c.recordId) ?? 0;
  2827. concernOrder.set(c.recordId, sortOrder + 1);
  2828. return prisma.serviceConcern.create({
  2829. data: {
  2830. serviceRecordId: c.recordId,
  2831. description: c.description,
  2832. cause: c.cause ?? null,
  2833. correction: c.correction ?? null,
  2834. confirmation: c.confirmation ?? null,
  2835. confirmedAt: c.confirmedAt ?? null,
  2836. confirmedById: c.confirmedAt ? USER_ID : null,
  2837. sortOrder,
  2838. },
  2839. });
  2840. }),
  2841. );
  2842. console.log(` Created ${concerns.length} concerns`);
  2843. // -- Job clock --
  2844. // Time the technicians clocked on jobs over the last few days, which the
  2845. // work order's clock panel and the manager's timesheets read. The demo
  2846. // owner has a clock running now (the pill in the header), on the Porsche
  2847. // inspection, which is therefore under way.
  2848. console.log("\nCreating time entries...");
  2849. await prisma.serviceRecord.update({ where: { id: unassignedRecords[2].id }, data: { status: "in-progress" satisfies JobStatus } });
  2850. type Clock = { recordId: string; tech: number; start: Date; end: Date | null; note?: string; source?: "app" | "web" | "manual" };
  2851. const clocks: Clock[] = [
  2852. { recordId: serviceRecords[12].id, tech: 1, start: at(-3, 8, 0), end: at(-3, 11, 40), note: "Turbo and manifold heat shield off" },
  2853. { recordId: serviceRecords[12].id, tech: 1, start: at(-2, 7, 35), end: at(-2, 12, 10), note: "New turbo and oil feed line fitted" },
  2854. { recordId: serviceRecords[12].id, tech: 1, start: at(-1, 13, 0), end: at(-1, 14, 5), note: "Road test and boost check", source: "manual" },
  2855. { recordId: boardServiceRecords[6].id, tech: 2, start: at(-2, 7, 0), end: at(-2, 15, 30), note: "Transmission out, teardown" },
  2856. { recordId: boardServiceRecords[6].id, tech: 2, start: at(-1, 7, 5), end: at(-1, 15, 45), note: "Clutch packs and seals" },
  2857. { recordId: serviceRecords[10].id, tech: 8, start: at(-1, 8, 0), end: at(-1, 10, 15) },
  2858. { recordId: unassignedRecords[11].id, tech: 10, start: at(-3, 9, 0), end: at(-3, 10, 30), note: "Diagnosis, pressures and compressor check", source: "web" },
  2859. { recordId: boardServiceRecords[1].id, tech: 1, start: minutesAgo(50), end: null },
  2860. { recordId: unassignedRecords[2].id, tech: 10, start: minutesAgo(25), end: null, source: "web" },
  2861. // Last week's finished jobs, clocked as they were planned
  2862. ...spreadRecords.slice(10, 15).flatMap((rec, i): Clock[] =>
  2863. rec.startDateTime && rec.endDateTime && rec.technicianId
  2864. ? [{ recordId: rec.id, tech: technicians.findIndex((t) => t.id === rec.technicianId), start: rec.startDateTime, end: new Date(rec.endDateTime.getTime() + ((i % 3) - 1) * 10 * 60_000) }]
  2865. : [],
  2866. ),
  2867. ];
  2868. await Promise.all(
  2869. clocks.map((c) =>
  2870. prisma.timeEntry.create({
  2871. data: {
  2872. organizationId: ORG_ID,
  2873. serviceRecordId: c.recordId,
  2874. technicianId: technicians[c.tech].id,
  2875. startedAt: c.start,
  2876. endedAt: c.end,
  2877. durationMinutes: c.end ? Math.round((c.end.getTime() - c.start.getTime()) / 60_000) : null,
  2878. note: c.note ?? null,
  2879. source: c.source ?? "app",
  2880. editedAt: c.source === "manual" ? c.end : null,
  2881. editedByUserId: c.source === "manual" ? USER_ID : null,
  2882. createdAt: c.start,
  2883. },
  2884. }),
  2885. ),
  2886. );
  2887. console.log(` Created ${clocks.length} time entries (${clocks.filter((c) => !c.end).length} running)`);
  2888. // -- Signature --
  2889. // The demo owner's saved signature (Settings -> Profile), which invoices,
  2890. // quotes and work orders print on the signature line for whoever opened
  2891. // the job, and open inspections preview for their technician.
  2892. const ownerMember = await prisma.organizationMember.findUniqueOrThrow({
  2893. where: { userId_organizationId: { userId: USER_ID, organizationId: ORG_ID } },
  2894. select: { id: true },
  2895. });
  2896. await prisma.memberSignature.create({
  2897. data: { memberId: ownerMember.id, mimeType: "image/png", data: new Uint8Array(signaturePng()) },
  2898. });
  2899. // Every seeded job was opened by the demo owner, so every sheet has a signer.
  2900. const opened = await prisma.serviceRecord.updateMany({ where: { organizationId: ORG_ID }, data: { createdById: USER_ID } });
  2901. console.log(`\nSaved the demo owner's signature, signer on ${opened.count} work orders`);
  2902. // -- Scheduled messages --
  2903. // The messages page and the calendar's message events both read these. The
  2904. // mix is deliberate: sent history behind us, a queue in front, a couple of
  2905. // recurring ones that keep rescheduling, plus one failure and one cancelled
  2906. // so the status filters have something in every bucket.
  2907. console.log("\nCreating scheduled messages...");
  2908. const scheduledMessages = await Promise.all([
  2909. // Already sent
  2910. prisma.scheduledMessage.create({ data: { organizationId: ORG_ID, createdById: USER_ID, channel: "sms", body: "Hi James, your Toyota Camry is booked in for Thursday at 08:00. Reply STOP to opt out.", customerId: customers[1].id, vehicleId: vehicles[0].id, status: "sent", sendAt: at(-9, 9), sentAt: at(-9, 9), lastRunAt: at(-9, 9), runCount: 1 } }),
  2911. prisma.scheduledMessage.create({ data: { organizationId: ORG_ID, createdById: USER_ID, channel: "email", subject: "Your invoice from Egeland Auto", body: "Hi Sarah,\n\nThe annual service on your Porsche 911 is complete and the invoice is attached. All OEM parts as always.\n\nEgeland Auto", customerId: customers[3].id, vehicleId: vehicles[13].id, status: "sent", sendAt: at(-6, 16), sentAt: at(-6, 16), lastRunAt: at(-6, 16), runCount: 1 } }),
  2912. prisma.scheduledMessage.create({ data: { organizationId: ORG_ID, createdById: USER_ID, channel: "sms", body: "Ridgeline: the Komatsu PC210 is ready for transport. Track chains and sprockets both replaced.", customerId: customers[8].id, vehicleId: vehicles[31].id, status: "sent", sendAt: at(-3, 15, 30), sentAt: at(-3, 15, 30), lastRunAt: at(-3, 15, 30), runCount: 1 } }),
  2913. // Queued for today and this week
  2914. prisma.scheduledMessage.create({ data: { organizationId: ORG_ID, createdById: USER_ID, channel: "sms", body: "Kevin, the Sprinter is finished and ready for collection any time before 17:00 today.", customerId: customers[15].id, vehicleId: vehicles[9].id, status: "scheduled", sendAt: at(0, 15) } }),
  2915. prisma.scheduledMessage.create({ data: { organizationId: ORG_ID, createdById: USER_ID, channel: "in_app", body: "Chase Metro City Services for the purchase order before starting the F-150 exhaust job.", status: "scheduled", sendAt: at(0, 16, 30) } }),
  2916. prisma.scheduledMessage.create({ data: { organizationId: ORG_ID, createdById: USER_ID, channel: "email", subject: `Quote Q-${YEAR}-003 - CAT D6 undercarriage`, body: "Hi,\n\nA reminder that the undercarriage rebuild quote for the CAT D6 expires in three weeks. Happy to walk through the wear measurements if that helps.\n\nEgeland Auto", customerId: customers[0].id, vehicleId: vehicles[29].id, status: "scheduled", sendAt: at(1, 9) } }),
  2917. prisma.scheduledMessage.create({ data: { organizationId: ORG_ID, createdById: USER_ID, channel: "sms", body: "Reminder: your Volvo FH 640 brake inspection is booked for tomorrow at 07:30.", customerId: customers[2].id, vehicleId: vehicles[18].id, status: "scheduled", sendAt: at(2, 8) } }),
  2918. prisma.scheduledMessage.create({ data: { organizationId: ORG_ID, createdById: USER_ID, channel: "email", subject: "Pre-planting service slots", body: "Hi,\n\nWe are holding two slots for the John Deere 6R and the Massey Ferguson ahead of planting. Let us know which week suits.\n\nEgeland Auto", customerId: customers[10].id, status: "scheduled", sendAt: at(3, 10) } }),
  2919. prisma.scheduledMessage.create({ data: { organizationId: ORG_ID, createdById: USER_ID, channel: "telegram", body: "Your Tesla Model Y is booked for a brake fluid test on Monday at 08:00.", customerId: customers[13].id, vehicleId: vehicles[7].id, status: "scheduled", sendAt: at(4, 9) } }),
  2920. prisma.scheduledMessage.create({ data: { organizationId: ORG_ID, createdById: USER_ID, channel: "whatsapp", body: "Hi Sarah, a reminder that the BMW 330i is booked in on Tuesday at 09:00 for the brake overhaul.", customerId: customers[3].id, vehicleId: vehicles[4].id, status: "scheduled", sendAt: at(5, 8) } }),
  2921. prisma.scheduledMessage.create({ data: { organizationId: ORG_ID, createdById: USER_ID, channel: "whatsapp", body: "The water pump is fitted and the A4 is ready whenever suits you.", customerId: customers[7].id, vehicleId: vehicles[10].id, status: "sent", sendAt: at(-1, 14), sentAt: at(-1, 14), lastRunAt: at(-1, 14), runCount: 1 } }),
  2922. // Further out
  2923. prisma.scheduledMessage.create({ data: { organizationId: ORG_ID, createdById: USER_ID, channel: "email", subject: "Track day prep - Porsche 911", body: "Hi Sarah,\n\nBooking your 911 in for brake pads, fluid and tire pressures the week before the track day. Reply with a day that suits.\n\nEgeland Auto", customerId: customers[3].id, vehicleId: vehicles[13].id, status: "scheduled", sendAt: at(9, 11) } }),
  2924. prisma.scheduledMessage.create({ data: { organizationId: ORG_ID, createdById: USER_ID, channel: "sms", body: "Titan Building: the Liebherr crane certification is due. Shall we book the external inspector?", customerId: customers[14].id, vehicleId: vehicles[36].id, status: "scheduled", sendAt: at(12, 10) } }),
  2925. prisma.scheduledMessage.create({ data: { organizationId: ORG_ID, createdById: USER_ID, channel: "email", subject: "Pre-harvest booking", body: "Hi,\n\nHarvest is close enough that the combine slots are filling. Reply to hold a week for the S780 and the X9.\n\nEgeland Auto", customerId: customers[18].id, status: "scheduled", sendAt: at(21, 9) } }),
  2926. // Recurring
  2927. prisma.scheduledMessage.create({ data: { organizationId: ORG_ID, createdById: USER_ID, channel: "email", subject: "Monthly fleet maintenance statement", body: "Attached is this month's statement across all Summit Construction units.\n\nEgeland Auto", customerId: customers[0].id, status: "scheduled", frequency: "monthly", sendAt: at(6, 8), endDate: months(12), lastRunAt: at(-24, 8), runCount: 4 } }),
  2928. prisma.scheduledMessage.create({ data: { organizationId: ORG_ID, createdById: USER_ID, channel: "sms", body: "Greenfield: quarterly PM window opens next week. Reply with the trucks you want in first.", customerId: customers[16].id, status: "scheduled", frequency: "monthly", sendAt: at(8, 9), lastRunAt: at(-22, 9), runCount: 2 } }),
  2929. prisma.scheduledMessage.create({ data: { organizationId: ORG_ID, createdById: USER_ID, channel: "in_app", body: "Weekly check: any job on the board still waiting on parts?", status: "scheduled", frequency: "weekly", sendAt: at(MONDAY_OFFSET + 4, 15), lastRunAt: at(MONDAY_OFFSET - 3, 15), runCount: 9 } }),
  2930. prisma.scheduledMessage.create({ data: { organizationId: ORG_ID, createdById: USER_ID, channel: "email", subject: "Annual roadworthiness test due", body: "Hi,\n\nYour vehicle's periodic technical inspection is due within the month. Reply to book a slot.\n\nEgeland Auto", customerId: customers[2].id, vehicleId: vehicles[17].id, status: "scheduled", frequency: "yearly", sendAt: at(30, 9), runCount: 1, lastRunAt: at(-335, 9) } }),
  2931. // The unhappy paths
  2932. prisma.scheduledMessage.create({ data: { organizationId: ORG_ID, createdById: USER_ID, channel: "sms", body: "Amanda, your Civic clutch quote is ready to view.", customerId: customers[9].id, vehicleId: vehicles[5].id, status: "failed", sendAt: at(-2, 11), lastRunAt: at(-2, 11), runCount: 1, errorMessage: "Carrier rejected the message: unknown destination number" } }),
  2933. prisma.scheduledMessage.create({ data: { organizationId: ORG_ID, createdById: USER_ID, channel: "email", subject: "Winter tire changeover", body: "Booking opens next week for the winter changeover.", customerId: customers[11].id, status: "cancelled", sendAt: at(5, 9) } }),
  2934. ]);
  2935. console.log(` Created ${scheduledMessages.length} scheduled messages`);
  2936. // -- Telegram --
  2937. console.log("\nCreating Telegram messages...");
  2938. await prisma.customer.update({ where: { id: customers[13].id }, data: { telegramChatId: "784512996" } });
  2939. await prisma.customer.update({ where: { id: customers[5].id }, data: { telegramChatId: "612447803" } });
  2940. await prisma.customer.update({ where: { id: customers[7].id }, data: { telegramChatId: "533901764" } });
  2941. await prisma.customer.update({ where: { id: customers[11].id }, data: { telegramChatId: "497260118" } });
  2942. const telegram = await Promise.all([
  2943. prisma.telegramMessage.create({ data: { organizationId: ORG_ID, customerId: customers[13].id, direction: "outbound", chatId: "784512996", body: "Hi Jessica, your Tesla Model Y is booked for a brake fluid test on Monday at 08:00.", status: "delivered", createdAt: hoursAgo(72) } }),
  2944. prisma.telegramMessage.create({ data: { organizationId: ORG_ID, customerId: customers[13].id, direction: "inbound", chatId: "784512996", body: "Perfect. Can you check the tire wear at the same time?", status: "received", createdAt: hoursAgo(71) } }),
  2945. prisma.telegramMessage.create({ data: { organizationId: ORG_ID, customerId: customers[13].id, direction: "outbound", chatId: "784512996", body: "Will do - we'll measure all four and send the numbers with the report.", status: "delivered", createdAt: hoursAgo(70) } }),
  2946. prisma.telegramMessage.create({ data: { organizationId: ORG_ID, customerId: customers[5].id, direction: "outbound", chatId: "612447803", body: "Ryan, the 12V auxiliary battery for the Model 3 is in stock. Want it done this week?", status: "delivered", createdAt: hoursAgo(26) } }),
  2947. prisma.telegramMessage.create({ data: { organizationId: ORG_ID, customerId: customers[5].id, direction: "inbound", chatId: "612447803", body: "Yes please. Thursday afternoon if you have it.", status: "received", createdAt: hoursAgo(25) } }),
  2948. prisma.telegramMessage.create({ data: { organizationId: ORG_ID, customerId: customers[5].id, direction: "outbound", chatId: "612447803", body: "Thursday 13:00 booked. It's a 30 minute job, you can wait if you like.", status: "delivered", createdAt: hoursAgo(24) } }),
  2949. prisma.telegramMessage.create({ data: { organizationId: ORG_ID, customerId: customers[7].id, direction: "inbound", chatId: "533901764", body: "The Golf is making a rattle on cold start that goes away after a minute. Worth looking at?", status: "received", createdAt: hoursAgo(9) } }),
  2950. prisma.telegramMessage.create({ data: { organizationId: ORG_ID, customerId: customers[7].id, direction: "outbound", chatId: "533901764", body: "Sounds like the timing chain tensioner, which is worth catching early on that engine. Can you drop it off Friday morning?", status: "delivered", createdAt: hoursAgo(8) } }),
  2951. prisma.telegramMessage.create({ data: { organizationId: ORG_ID, customerId: customers[7].id, direction: "inbound", chatId: "533901764", body: "Friday works. I'll leave the key in the box if you're busy.", status: "received", createdAt: hoursAgo(7) } }),
  2952. prisma.telegramMessage.create({ data: { organizationId: ORG_ID, customerId: customers[11].id, direction: "outbound", chatId: "497260118", body: "Mike, the Wrangler is back on the ground. Lift is torqued and aligned, and we road tested it.", status: "delivered", createdAt: hoursAgo(4) } }),
  2953. prisma.telegramMessage.create({ data: { organizationId: ORG_ID, customerId: customers[11].id, direction: "inbound", chatId: "497260118", body: "Brilliant, thanks. Picking it up around 16:00.", status: "received", createdAt: hoursAgo(3) } }),
  2954. // A send that did not land, so the failure state is visible somewhere.
  2955. prisma.telegramMessage.create({ data: { organizationId: ORG_ID, customerId: customers[11].id, direction: "outbound", chatId: "497260118", body: "Invoice is ready whenever you are.", status: "failed", errorMessage: "Bot was blocked by the user", createdAt: hoursAgo(2) } }),
  2956. ]);
  2957. console.log(` Created ${telegram.length} Telegram messages`);
  2958. // -- WhatsApp --
  2959. // WhatsApp only lets a business write freely for 24 hours after the
  2960. // customer's last message, so the conversations below deliberately sit on
  2961. // both sides of that line: some recent enough to reply to in the composer,
  2962. // and one older, where the last message out had to go as an approved
  2963. // template. Without that, the window rule is invisible on the demo and the
  2964. // "sent as template" label never appears.
  2965. console.log("\nCreating WhatsApp messages...");
  2966. const waFrom = "+15555550100";
  2967. // Customer 1001 is +1 (555) 555-0101 and they run in order from there. The
  2968. // 555-01xx block is the reserved fictional range, so none of these can ring.
  2969. const waTo = (index: number) => `+1555555${String(101 + index).padStart(4, "0")}`;
  2970. type WaExtra = {
  2971. status?: string;
  2972. errorMessage?: string;
  2973. templateName?: string;
  2974. mediaUrl?: string;
  2975. mediaType?: string;
  2976. mediaFilename?: string;
  2977. };
  2978. /** `who` is an index into `customers`, or a bare number for a stranger. */
  2979. const wa = (
  2980. who: number | string,
  2981. direction: "inbound" | "outbound",
  2982. body: string,
  2983. createdAt: Date,
  2984. extra: WaExtra = {},
  2985. ) => {
  2986. const number = typeof who === "number" ? waTo(who) : who;
  2987. return prisma.whatsappMessage.create({
  2988. data: {
  2989. organizationId: ORG_ID,
  2990. provider: "meta",
  2991. direction,
  2992. fromNumber: direction === "outbound" ? waFrom : number,
  2993. toNumber: direction === "outbound" ? number : waFrom,
  2994. body,
  2995. status: direction === "outbound" ? "read" : "received",
  2996. customerId: typeof who === "number" ? customers[who].id : null,
  2997. createdAt,
  2998. ...extra,
  2999. },
  3000. });
  3001. };
  3002. const whatsapp = await Promise.all([
  3003. // Sarah Coleman — inside the 24 hour window, so the composer is open and a
  3004. // visitor can type a reply straight away.
  3005. wa(3, "outbound", "Hi Sarah, the 911 is done. Pads, discs and a fluid flush, and the alignment came out inside spec.", hoursAgo(5)),
  3006. wa(3, "inbound", "Wonderful. Did the front tires make it through?", hoursAgo(4)),
  3007. wa(3, "outbound", "They did, about 8,000 miles left. Here it is back on the ground.", hoursAgo(4), {
  3008. mediaUrl: img("vehicles", "porsche-911.jpg"),
  3009. mediaType: "image",
  3010. mediaFilename: "porsche-911-collected.jpg",
  3011. }),
  3012. wa(3, "inbound", "Looks great. I'll collect it tomorrow morning.", minutesAgo(35)),
  3013. // David Chen — a problem reported, collected and quoted inside one thread,
  3014. // which is the shape most of this traffic actually takes.
  3015. wa(7, "inbound", "There's a puddle under the A4 this morning. Coolant, I think, it's pink.", hoursAgo(20)),
  3016. wa(7, "outbound", "Pink is the right colour for that engine, so it is coolant. Don't drive it far. Can we collect it?", hoursAgo(20)),
  3017. wa(7, "inbound", "Yes please. It's on the driveway, key under the mat.", hoursAgo(19)),
  3018. wa(7, "outbound", "Picked up. Water pump is weeping at the seal. Quote is $640 all in, and we have the pump on the shelf.", hoursAgo(6)),
  3019. // Summit Construction — fleet traffic, several vehicles in one thread.
  3020. wa(0, "outbound", "Morning. The Mack Granite passed its DOT inspection, paperwork is in the cab.", hoursAgo(30)),
  3021. wa(0, "inbound", "Good news. The F-150 on CO-4455 is due too, can it come in Thursday?", hoursAgo(29)),
  3022. wa(0, "outbound", "Thursday 07:00 works. Send it with the fuel card and we'll top it up.", hoursAgo(29)),
  3023. // Kevin O'Brien — the window has closed, so the last message out had to be
  3024. // an approved template rather than free text.
  3025. wa(15, "inbound", "Any chance the Sprinter is ready? I have an event Saturday.", hoursAgo(96)),
  3026. wa(15, "outbound", "Turbo is fitted and the oil feed line is flushed. Road tested clean, no smoke under boost.", hoursAgo(95)),
  3027. wa(15, "outbound", "Hi Kevin, your 2022 Mercedes-Benz Sprinter 316 CDI is ready for collection.", hoursAgo(11), {
  3028. templateName: "vehicle_ready",
  3029. status: "delivered",
  3030. }),
  3031. // Amanda Foster — a send that bounced, so the failure state is on screen.
  3032. wa(9, "outbound", "Hi Amanda, the clutch quote for the Civic is ready to look at.", hoursAgo(13), {
  3033. status: "failed",
  3034. errorMessage: "(#131026) Message undeliverable: the recipient is not on WhatsApp",
  3035. }),
  3036. // Someone the workshop has never dealt with, which is what most first
  3037. // contact actually looks like. No customer record behind it yet.
  3038. wa("+15555550188", "inbound", "Hi, do you take walk-ins for a brake noise? Driving past this afternoon.", hoursAgo(2)),
  3039. ]);
  3040. console.log(` Created ${whatsapp.length} WhatsApp messages`);
  3041. // -- Notifications --
  3042. // The bell is empty on a fresh reset, which makes the app look idle.
  3043. console.log("\nCreating notifications...");
  3044. const notifications = await Promise.all([
  3045. prisma.notification.create({ data: { organizationId: ORG_ID, type: "sms_inbound", title: "New SMS from Summit Construction", message: "Thanks! We'll send someone before 4pm. Can you look at the D6 bulldozer next week?", entityType: "customer", entityId: customers[0].id, entityUrl: `/customers/${customers[0].id}`, read: false, createdAt: minutesAgo(58) } }),
  3046. prisma.notification.create({ data: { organizationId: ORG_ID, type: "inspection_quote_request", title: "Quote requested from an inspection", message: "Amanda Foster asked for a quote on two items from the Honda Civic inspection.", entityType: "inspection", entityId: inspections[2].id, entityUrl: `/inspections/${inspections[2].id}`, read: false, createdAt: hoursAgo(3) } }),
  3047. prisma.notification.create({ data: { organizationId: ORG_ID, type: "inventory.lowStock", title: "Low stock: DPF Pressure Sensor", message: "1 in stock, reorder point is 3.", entityType: "inventory", entityId: inventoryParts[21].id, entityUrl: `/inventory`, read: false, createdAt: hoursAgo(6) } }),
  3048. prisma.notification.create({ data: { organizationId: ORG_ID, type: "reminder.due", title: "Reminder due today", message: "Volvo FH 640 - brake inspection before the next long-haul run.", entityType: "reminder", entityId: coreReminders[5].id, entityUrl: `/reminders`, read: false, createdAt: at(0, 7) } }),
  3049. prisma.notification.create({ data: { organizationId: ORG_ID, type: "service_request", title: "New service request", message: "Mike Thompson asked for a booking for the Jeep Wrangler Rubicon.", entityType: "service_request", entityId: vehicles[11].id, entityUrl: `/work-orders`, read: false, createdAt: hoursAgo(19) } }),
  3050. prisma.notification.create({ data: { organizationId: ORG_ID, type: "quote_response", title: "Quote accepted", message: `Jeep Wrangler - Winch Install (Q-${YEAR}-011) was accepted by Mike Thompson.`, entityType: "quote", entityId: quotes[10].id, entityUrl: `/quotes/${quotes[10].id}`, read: true, createdAt: hoursAgo(30) } }),
  3051. prisma.notification.create({ data: { organizationId: ORG_ID, type: "invoice_payment", title: "Payment received", message: "Pacific Freight Lines settled the Kenworth T680 engine service invoice.", entityType: "invoice", entityId: serviceRecords[4].id, entityUrl: `/work-orders/${serviceRecords[4].id}`, read: true, createdAt: hoursAgo(50) } }),
  3052. prisma.notification.create({ data: { organizationId: ORG_ID, type: "telegram_inbound", title: "New Telegram message", message: "Jessica Rivera: Can you check the tire wear at the same time?", entityType: "customer", entityId: customers[13].id, entityUrl: `/customers/${customers[13].id}`, read: true, createdAt: hoursAgo(71) } }),
  3053. prisma.notification.create({ data: { organizationId: ORG_ID, type: "whatsapp_inbound", title: "New WhatsApp message", message: "Sarah Coleman: Looks great. I'll collect it tomorrow morning.", entityType: "whatsapp_message", entityId: whatsapp[3].id, entityUrl: `/messages?tab=whatsapp&customerId=${customers[3].id}`, read: false, createdAt: minutesAgo(35) } }),
  3054. // Nobody on file behind this one, which is what the link has to cope with.
  3055. prisma.notification.create({ data: { organizationId: ORG_ID, type: "whatsapp_inbound", title: "New WhatsApp message", message: "+15555550188: Hi, do you take walk-ins for a brake noise? Driving past this afternoon.", entityType: "whatsapp_message", entityId: whatsapp[whatsapp.length - 1].id, entityUrl: "/messages?tab=whatsapp", read: false, createdAt: hoursAgo(2) } }),
  3056. prisma.notification.create({ data: { organizationId: ORG_ID, type: "status_report_feedback", title: "Customer replied to a status report", message: "Lisa Martinez: \"Thanks for the video, that makes sense. Go ahead.\"", entityType: "service_record", entityId: serviceRecords[0].id, entityUrl: `/work-orders/${serviceRecords[0].id}`, read: true, createdAt: hoursAgo(96) } }),
  3057. ]);
  3058. console.log(` Created ${notifications.length} notifications`);
  3059. // -- Labor presets --
  3060. // cleanup() drops these on every reset, so the page is otherwise empty.
  3061. console.log("\nCreating labor presets...");
  3062. const laborPresets = await Promise.all([
  3063. prisma.laborPreset.create({ data: { organizationId: ORG_ID, userId: USER_ID, name: "Oil service - light vehicle", description: "Standard oil and filter change with a multi-point check.",
  3064. items: { create: [{ description: "Oil and filter change", hours: 0.5, rate: 110, sortOrder: 0 }, { description: "Multi-point inspection", hours: 0.5, rate: 110, sortOrder: 1 }] },
  3065. parts: { create: [{ name: "Synthetic Engine Oil 5W-30 (5qt)", partNumber: "OIL-5W30-5Q", quantity: 1, unit: "pcs", unitPrice: 45, sortOrder: 0, inventoryPartId: inventoryParts[0].id }, { name: "Oil Filter - European Vehicles", partNumber: "OF-EUR-01", quantity: 1, unit: "pcs", unitPrice: 15.5, sortOrder: 1, inventoryPartId: inventoryParts[6].id }] } } }),
  3066. prisma.laborPreset.create({ data: { organizationId: ORG_ID, userId: USER_ID, name: "Front brake service", description: "Pads and rotors front axle, including road test.",
  3067. items: { create: [{ description: "Front pad and rotor replacement", hours: 2, rate: 110, sortOrder: 0 }, { description: "Brake road test", hours: 0.3, rate: 110, sortOrder: 1 }] },
  3068. parts: { create: [{ name: "Brake Pad Set - Universal Light Vehicle", partNumber: "BP-UNI-LV", quantity: 1, unit: "pcs", unitPrice: 68, sortOrder: 0, inventoryPartId: inventoryParts[4].id }] } } }),
  3069. prisma.laborPreset.create({ data: { organizationId: ORG_ID, userId: USER_ID, name: "Heavy truck PM service", description: "Full preventive maintenance for a class 8 tractor unit.",
  3070. items: { create: [{ description: "Engine oil and all filters", hours: 2.5, rate: 140, sortOrder: 0 }, { description: "Grease service, all points", hours: 1, rate: 140, sortOrder: 1 }, { description: "Brake adjustment and lining measurement", hours: 1.5, rate: 140, sortOrder: 2 }] },
  3071. parts: { create: [{ name: "Heavy Duty Engine Oil 15W-40 (5gal)", partNumber: "OIL-15W40-5G", quantity: 2, unit: "pcs", unitPrice: 185, sortOrder: 0, inventoryPartId: inventoryParts[2].id }, { name: "Fuel Filter - Diesel Universal", partNumber: "FF-DSL-UNI", quantity: 2, unit: "pcs", unitPrice: 29, sortOrder: 1, inventoryPartId: inventoryParts[18].id }] } } }),
  3072. prisma.laborPreset.create({ data: { organizationId: ORG_ID, userId: USER_ID, name: "Excavator 1000hr service", description: "Fluids, filters, undercarriage and pin play check.",
  3073. items: { create: [{ description: "All fluids and filters", hours: 3, rate: 140, sortOrder: 0 }, { description: "Undercarriage and pin play inspection", hours: 1.5, rate: 140, sortOrder: 1 }] },
  3074. parts: { create: [{ name: "Hydraulic Filter - CAT/Komatsu", partNumber: "HF-CAT-KOM", quantity: 2, unit: "pcs", unitPrice: 120, sortOrder: 0, inventoryPartId: inventoryParts[8].id }, { name: "Air Filter - Heavy Equipment", partNumber: "AF-HE-UNI", quantity: 1, unit: "pcs", unitPrice: 85, sortOrder: 1, inventoryPartId: inventoryParts[19].id }] } } }),
  3075. prisma.laborPreset.create({ data: { organizationId: ORG_ID, userId: USER_ID, name: "A/C service - R-1234yf recharge", description: "Evacuate, vacuum test, recharge by weight. Refrigerant books out of stock by the pound.",
  3076. items: { create: [{ description: "Evacuate and vacuum test", hours: 0.8, rate: 110, sortOrder: 0 }, { description: "Recharge and performance check", hours: 0.7, rate: 110, sortOrder: 1 }] },
  3077. parts: { create: [{ name: "Refrigerant R-1234yf (bulk)", partNumber: "R1234YF-BULK", quantity: 1.4, unit: "lb", unitPrice: 68, sortOrder: 0, inventoryPartId: inventoryParts[26].id }] } } }),
  3078. prisma.laborPreset.create({ data: { organizationId: ORG_ID, userId: USER_ID, name: "Seasonal tire changeover", description: "Wheel swap, balance and pressure set, four wheels.",
  3079. items: { create: [{ description: "Wheel swap and balance (4)", hours: 1, rate: 95, sortOrder: 0 }, { description: "Pressure set and torque check", hours: 0.2, rate: 95, sortOrder: 1 }] },
  3080. parts: { create: [] } } }),
  3081. ]);
  3082. console.log(` Created ${laborPresets.length} labor presets`);
  3083. // -- Roles --
  3084. // The demo has one member, so the team page shows nothing about the
  3085. // permission model unless the roles themselves are there to open. Members
  3086. // are deliberately not seeded: each one needs a real User row, and inviting
  3087. // is blocked in demo mode anyway.
  3088. console.log("\nCreating roles...");
  3089. const roles = await Promise.all([
  3090. prisma.role.create({ data: { organizationId: ORG_ID, name: "Service advisor", permissions: { create: [
  3091. { action: "read", subject: "dashboard" },
  3092. { action: "read", subject: "vehicles" }, { action: "update", subject: "vehicles" },
  3093. { action: "create", subject: "customers" }, { action: "read", subject: "customers" }, { action: "update", subject: "customers" },
  3094. { action: "create", subject: "work_orders" }, { action: "read", subject: "work_orders" }, { action: "update", subject: "work_orders" },
  3095. { action: "create", subject: "quotes" }, { action: "read", subject: "quotes" }, { action: "update", subject: "quotes" },
  3096. { action: "read", subject: "work_board" }, { action: "read", subject: "inventory" }, { action: "read", subject: "tire_hotel" },
  3097. ] } } }),
  3098. prisma.role.create({ data: { organizationId: ORG_ID, name: "Technician", permissions: { create: [
  3099. { action: "read", subject: "dashboard" },
  3100. { action: "read", subject: "vehicles" },
  3101. { action: "read", subject: "customers" },
  3102. { action: "read", subject: "work_orders" }, { action: "update", subject: "work_orders" },
  3103. { action: "read", subject: "work_board" }, { action: "update", subject: "work_board" },
  3104. { action: "create", subject: "inspections" }, { action: "read", subject: "inspections" }, { action: "update", subject: "inspections" },
  3105. { action: "read", subject: "inventory" },
  3106. { action: "read", subject: "tire_hotel" }, { action: "update", subject: "tire_hotel" },
  3107. ] } } }),
  3108. prisma.role.create({ data: { organizationId: ORG_ID, name: "Bookkeeper", permissions: { create: [
  3109. { action: "read", subject: "dashboard" },
  3110. { action: "read", subject: "customers" },
  3111. { action: "read", subject: "work_orders" },
  3112. { action: "read", subject: "billing" }, { action: "update", subject: "billing" },
  3113. { action: "read", subject: "reports" },
  3114. ] } } }),
  3115. ]);
  3116. console.log(` Created ${roles.length} roles`);
  3117. // -- Webhooks --
  3118. // Left inactive on purpose. The dispatcher and the delivery worker both skip
  3119. // an inactive webhook, so the page shows a real integration with real
  3120. // delivery history without the demo posting anything at a stranger's URL.
  3121. console.log("\nCreating webhooks...");
  3122. const webhooks = await Promise.all([
  3123. prisma.webhook.create({ data: { organizationId: ORG_ID, createdById: USER_ID, name: "Accounting sync", url: "https://hooks.example.com/torqvoice/accounting", secret: randomBytes(24).toString("hex"), events: JSON.stringify(["payment.create", "quote.status", "service.status"]), description: "Pushes every payment and quote decision into the bookkeeping ledger.", isActive: false, lastTriggeredAt: hoursAgo(5), lastSuccessAt: hoursAgo(5) } }),
  3124. prisma.webhook.create({ data: { organizationId: ORG_ID, createdById: USER_ID, name: "Fleet portal", url: "https://hooks.example.com/summit/work-orders", secret: randomBytes(24).toString("hex"), events: JSON.stringify(["service.status", "vehicle.update"]), description: "Tells Summit Construction's own system when one of their units is finished.", isActive: false, lastTriggeredAt: hoursAgo(31), lastSuccessAt: hoursAgo(31), failureCount: 1, lastFailureAt: hoursAgo(52) } }),
  3125. ]);
  3126. // Payloads match what dispatchWebhookEvent actually sends, so the delivery
  3127. // detail view shows the real envelope rather than an invented one.
  3128. const deliveryPayload = (event: string, entity: string, entityId: string, message: string, data: Record<string, unknown>, sentAt: Date) =>
  3129. JSON.stringify({ id: randomBytes(12).toString("hex"), event, createdAt: sentAt.toISOString(), organizationId: ORG_ID, entity, entityId, message, userId: USER_ID, data });
  3130. await Promise.all([
  3131. prisma.webhookDelivery.create({ data: { webhookId: webhooks[0].id, event: "payment.create", payload: deliveryPayload("payment.create", "payment", serviceRecords[12].id, "Payment recorded on the Sprinter turbo invoice", { amount: 2243.7, currency: "USD", method: "card" }, hoursAgo(5)), status: "success", statusCode: 200, attempt: 1, durationMs: 214, deliveredAt: hoursAgo(5), createdAt: hoursAgo(5) } }),
  3132. prisma.webhookDelivery.create({ data: { webhookId: webhooks[0].id, event: "quote.status", payload: deliveryPayload("quote.status", "quote", quotes[10].id, `Quote Q-${YEAR}-011 was accepted`, { status: "accepted", total: 3180 }, hoursAgo(30)), status: "success", statusCode: 200, attempt: 1, durationMs: 187, deliveredAt: hoursAgo(30), createdAt: hoursAgo(30) } }),
  3133. prisma.webhookDelivery.create({ data: { webhookId: webhooks[1].id, event: "service.status", payload: deliveryPayload("service.status", "service_record", serviceRecords[0].id, "Transmission flush completed", { status: "completed", licensePlate: "CO-4455" }, hoursAgo(31)), status: "success", statusCode: 202, attempt: 2, durationMs: 1340, deliveredAt: hoursAgo(31), createdAt: hoursAgo(31) } }),
  3134. // Five attempts, all timed out. This is the row that pushed failureCount up.
  3135. prisma.webhookDelivery.create({ data: { webhookId: webhooks[1].id, event: "service.status", payload: deliveryPayload("service.status", "service_record", serviceRecords[4].id, "DOT inspection completed", { status: "completed", licensePlate: "AZ-9901" }, hoursAgo(52)), status: "failed", statusCode: 504, errorMessage: "Gateway timeout after 10000ms", attempt: 5, maxAttempts: 5, durationMs: 10000, createdAt: hoursAgo(52) } }),
  3136. ]);
  3137. console.log(` Created ${webhooks.length} webhooks`);
  3138. // -- Report schedules --
  3139. console.log("\nCreating report schedules...");
  3140. const reportSchedules = await Promise.all([
  3141. prisma.reportSchedule.create({ data: { organizationId: ORG_ID, createdById: USER_ID, name: "Monday morning summary", frequency: "weekly", dateRange: "last7d", sections: JSON.stringify(["revenue", "services", "technicians", "pastDue"]), recipients: JSON.stringify([USER_ID]), nextRunDate: at(MONDAY_OFFSET + 7, 7), lastRunAt: at(MONDAY_OFFSET, 7), runCount: 12, isActive: true } }),
  3142. prisma.reportSchedule.create({ data: { organizationId: ORG_ID, createdById: USER_ID, name: "Month-end financials", frequency: "monthly", dateRange: "last30d", sections: JSON.stringify(["revenue", "tax", "parts", "inventory"]), recipients: JSON.stringify([USER_ID]), nextRunDate: months(1), lastRunAt: months(-1), runCount: 5, isActive: true } }),
  3143. prisma.reportSchedule.create({ data: { organizationId: ORG_ID, createdById: USER_ID, name: "Quarterly customer retention", frequency: "quarterly", dateRange: "last90d", sections: JSON.stringify(["customers", "retention", "jobAnalytics"]), recipients: JSON.stringify([USER_ID]), nextRunDate: months(2), runCount: 0, isActive: false } }),
  3144. ]);
  3145. console.log(` Created ${reportSchedules.length} report schedules`);
  3146. // -- Recurring invoices --
  3147. console.log("\nCreating recurring invoices...");
  3148. const recurring = await Promise.all([
  3149. prisma.recurringInvoice.create({ data: { vehicleId: vehicles[17].id, title: "Kenworth T680 - monthly PM", description: "Contracted monthly preventive maintenance for Pacific Freight unit #3.", frequency: "monthly", type: "maintenance", taxRate: 8, nextRunDate: days(11), lastRunAt: days(-19), runCount: 7, invoiceNotes: "Fleet contract rate. Net-30.",
  3150. templateParts: { create: [{ name: "Heavy Duty Engine Oil 15W-40 (5gal)", partNumber: "OIL-15W40-5G", quantity: 2, unitPrice: 185 }, { name: "Fuel Filter - Diesel Universal", partNumber: "FF-DSL-UNI", quantity: 2, unitPrice: 29 }] },
  3151. templateLabor: { create: [{ description: "Full PM service", hours: 4, rate: 140 }] } } }),
  3152. prisma.recurringInvoice.create({ data: { vehicleId: vehicles[3].id, title: "Metro City F-150 - quarterly service", description: "Municipal contract. PO number required on every invoice.", frequency: "quarterly", type: "maintenance", taxRate: 8, nextRunDate: days(34), lastRunAt: days(-57), runCount: 3, invoiceNotes: "Quote the city asset tag on the invoice.",
  3153. templateParts: { create: [{ name: "Synthetic Engine Oil 5W-30 (5qt)", partNumber: "OIL-5W30-5Q", quantity: 2, unitPrice: 45 }] },
  3154. templateLabor: { create: [{ description: "Quarterly service and inspection", hours: 2, rate: 110 }] } } }),
  3155. prisma.recurringInvoice.create({ data: { vehicleId: vehicles[36].id, title: "Liebherr crane - annual certification", description: "Annual load test and certification for Titan Building.", frequency: "yearly", type: "inspection", taxRate: 8, nextRunDate: days(53), runCount: 1, lastRunAt: days(-312),
  3156. templateParts: { create: [] },
  3157. templateLabor: { create: [{ description: "Load test, certification and paperwork", hours: 8, rate: 150 }] } } }),
  3158. ]);
  3159. console.log(` Created ${recurring.length} recurring invoices`);
  3160. // -- Custom fields --
  3161. console.log("\nCreating custom fields...");
  3162. const customFields = await Promise.all([
  3163. prisma.customFieldDefinition.create({ data: { organizationId: ORG_ID, userId: USER_ID, name: "purchase_order", label: "Purchase order number", fieldType: "text", entityType: "service_record", sortOrder: 0, required: false } }),
  3164. // No "Workshop bay" field: bays are a feature of their own now (Work
  3165. // bays, seeded above), and a select field beside it would say the same
  3166. // thing in a second place.
  3167. prisma.customFieldDefinition.create({ data: { organizationId: ORG_ID, userId: USER_ID, name: "courtesy_vehicle", label: "Courtesy vehicle issued", fieldType: "checkbox", entityType: "service_record", sortOrder: 1, required: false } }),
  3168. prisma.customFieldDefinition.create({ data: { organizationId: ORG_ID, userId: USER_ID, name: "site_contact", label: "Site contact", fieldType: "text", entityType: "quote", sortOrder: 0, required: false } }),
  3169. ]);
  3170. await Promise.all([
  3171. prisma.customFieldValue.create({ data: { fieldId: customFields[0].id, entityId: serviceRecords[5].id, entityType: "service_record", value: "PO-44821" } }),
  3172. prisma.customFieldValue.create({ data: { fieldId: customFields[1].id, entityId: serviceRecords[12].id, entityType: "service_record", value: "true" } }),
  3173. prisma.customFieldValue.create({ data: { fieldId: customFields[2].id, entityId: quotes[2].id, entityType: "quote", value: "Dave Ruiz, site foreman - (555) 100-2044" } }),
  3174. ]);
  3175. console.log(` Created ${customFields.length} custom fields`);
  3176. // -- Service requests from the customer portal --
  3177. console.log("\nCreating portal service requests...");
  3178. const serviceRequests = await Promise.all([
  3179. prisma.serviceRequest.create({ data: { organizationId: ORG_ID, customerId: customers[11].id, vehicleId: vehicles[11].id, description: "Steering feels loose above 55 mph and there's a shimmy over expansion joints. Can you look before the weekend?", preferredDate: days(2), status: "pending", createdAt: hoursAgo(19) } }),
  3180. prisma.serviceRequest.create({ data: { organizationId: ORG_ID, customerId: customers[9].id, vehicleId: vehicles[5].id, description: "Clutch is slipping in third and fourth. I'd like the quote you mentioned turned into a booking.", preferredDate: days(6), status: "pending", createdAt: hoursAgo(41) } }),
  3181. prisma.serviceRequest.create({ data: { organizationId: ORG_ID, customerId: customers[19].id, vehicleId: vehicles[8].id, description: "Due for an oil change and the trailer plug is intermittent. Ranch use, so any weekday works.", preferredDate: days(9), status: "scheduled", adminNotes: "Booked for Tuesday 09:00 with Jake.", createdAt: hoursAgo(70) } }),
  3182. prisma.serviceRequest.create({ data: { organizationId: ORG_ID, customerId: customers[5].id, vehicleId: vehicles[6].id, description: "Low voltage warning on the Model 3 again. I think it's the 12V battery.", status: "completed", adminNotes: "12V battery replaced, tested good.", createdAt: hoursAgo(150) } }),
  3183. ]);
  3184. console.log(` Created ${serviceRequests.length} service requests`);
  3185. // -- Status reports --
  3186. console.log("\nCreating status reports...");
  3187. const statusReports = await Promise.all([
  3188. prisma.statusReport.create({ data: { organizationId: ORG_ID, serviceRecordId: serviceRecords[12].id, technicianId: technicians[1].id, title: "Sprinter turbo - what we found", message: "Shaft play was well past spec and the compressor wheel had contacted the housing. Replacement turbo is fitted and the oil feed line was flushed. Road tested, no smoke under boost.", status: "viewed", sentVia: "sms", publicToken: randomBytes(16).toString("hex"), sentAt: hoursAgo(52), viewedAt: hoursAgo(50), expiresAt: days(60) } }),
  3189. prisma.statusReport.create({ data: { organizationId: ORG_ID, serviceRecordId: serviceRecords[1].id, technicianId: technicians[1].id, title: "BMW 330i - 50,000 mile service", message: "Everything on the schedule is done: pads, rotors, plugs and a full inspection. Front tires have about 8,000 miles left in them.", status: "viewed", sentVia: "email", publicToken: randomBytes(16).toString("hex"), sentAt: hoursAgo(120), viewedAt: hoursAgo(117), customerFeedback: "Thanks, that's clear. Book me in when the tires get close.", feedbackAt: hoursAgo(116), expiresAt: days(45) } }),
  3190. prisma.statusReport.create({ data: { organizationId: ORG_ID, serviceRecordId: serviceRecords[0].id, technicianId: technicians[0].id, title: "Camry - oil service done", message: "Oil and filter changed, tires rotated. Nothing else to report.", status: "sent", sentVia: "sms,email", publicToken: randomBytes(16).toString("hex"), sentAt: hoursAgo(96), expiresAt: days(30) } }),
  3191. prisma.statusReport.create({ data: { organizationId: ORG_ID, serviceRecordId: unassignedRecords[11].id, technicianId: technicians[10].id, title: "Kenworth sleeper A/C - parts on order", message: "The compressor has seized and put debris through the condenser, so both need replacing. Parts land Wednesday and we'll have it back to you Thursday.", status: "draft", publicToken: randomBytes(16).toString("hex") } }),
  3192. ]);
  3193. console.log(` Created ${statusReports.length} status reports`);
  3194. // -- Tire hotel --
  3195. // Seasonal tire storage. The interesting parts of this module only exist
  3196. // once there is history behind a set: shelves filling up, tread wearing down
  3197. // over successive winters, and the replacement forecast that falls out of
  3198. // those two numbers. A set checked in yesterday shows none of it, so most of
  3199. // these carry two or three years of readings.
  3200. //
  3201. // Dates are relative rather than pinned to real April and October, so the
  3202. // demo reads the same in February as it does in August. A shop in the
  3203. // southern hemisphere sees the same story with the seasons the other way up.
  3204. console.log("\nCreating tire hotel...");
  3205. const tireWarehouse = await prisma.tireWarehouse.create({
  3206. data: {
  3207. name: "Tire store",
  3208. address: "1450 Foundry Road, Denver, CO 80216 (rear building)",
  3209. notes: "Unheated but dry. Racks A and B, floor level to head height.",
  3210. isDefault: true,
  3211. organizationId: ORG_ID,
  3212. userId: USER_ID,
  3213. },
  3214. });
  3215. // Two racks. The A bays are the small ones by the door and fill up first,
  3216. // which is why several of them read full while rack B has room.
  3217. const tireLocations = await Promise.all(
  3218. [
  3219. { code: "A-1-1-L", zone: "Rack A", rack: "1", shelf: "1", position: "Left", capacity: 8 },
  3220. { code: "A-1-1-R", zone: "Rack A", rack: "1", shelf: "1", position: "Right", capacity: 8 },
  3221. { code: "A-1-2-L", zone: "Rack A", rack: "1", shelf: "2", position: "Left", capacity: 8 },
  3222. { code: "A-1-2-R", zone: "Rack A", rack: "1", shelf: "2", position: "Right", capacity: 8 },
  3223. { code: "A-2-1-L", zone: "Rack A", rack: "2", shelf: "1", position: "Left", capacity: 8 },
  3224. { code: "A-2-1-R", zone: "Rack A", rack: "2", shelf: "1", position: "Right", capacity: 8 },
  3225. { code: "B-1-1-L", zone: "Rack B", rack: "1", shelf: "1", position: "Left", capacity: 12 },
  3226. { code: "B-1-1-R", zone: "Rack B", rack: "1", shelf: "1", position: "Right", capacity: 12 },
  3227. { code: "B-1-2-L", zone: "Rack B", rack: "1", shelf: "2", position: "Left", capacity: 12 },
  3228. {
  3229. code: "B-1-2-R",
  3230. zone: "Rack B",
  3231. rack: "1",
  3232. shelf: "2",
  3233. position: "Right",
  3234. capacity: 12,
  3235. notes: "Kept clear for walk-ins during the changeover weeks.",
  3236. },
  3237. ].map((spec) =>
  3238. prisma.tireLocation.create({
  3239. data: { ...spec, warehouseId: tireWarehouse.id, organizationId: ORG_ID },
  3240. }),
  3241. ),
  3242. );
  3243. const shelf = new Map(tireLocations.map((l) => [l.code, l]));
  3244. /**
  3245. * One stored set, described the way the workshop would describe it.
  3246. *
  3247. * `rounds` are tread readings in millimetres, newest first, four per round
  3248. * in the order a technician walks the car: front left, front right, rear
  3249. * left, rear right. Each round is one check-in a year earlier than the last,
  3250. * so the gaps between them are a winter's driving.
  3251. */
  3252. type TireSetSpec = {
  3253. vehicle: number;
  3254. customer: number;
  3255. season: "summer" | "winter" | "all_season";
  3256. studded?: boolean;
  3257. brand: string;
  3258. model: string;
  3259. size: string;
  3260. dotCode: string;
  3261. loadSpeedIndex?: string;
  3262. withRims: boolean;
  3263. rimType?: string;
  3264. hasTpms?: boolean;
  3265. quantity?: number;
  3266. /** Shelf code, or null for a set that is no longer on a shelf. */
  3267. location: string | null;
  3268. /** Where it used to sit, for one that has left. History still shows it. */
  3269. lastShelf?: string;
  3270. /** Bar, as found at check-in. Vans and trucks run higher than cars. */
  3271. pressureBar?: number;
  3272. status?: "stored" | "released" | "disposed";
  3273. /** Days ago the most recent check-in happened. */
  3274. storedDaysAgo: number;
  3275. /** Days ago the set left, for one that is no longer here. */
  3276. goneDaysAgo?: number;
  3277. rounds: number[][];
  3278. treatments?: Partial<Record<"wash_tires" | "wash_rims" | "balance" | "tpms_service" | "new_valves" | "repair", "pending" | "done" | "skipped">>;
  3279. notes?: string;
  3280. damage?: string;
  3281. };
  3282. const TIRE_SETS: TireSetSpec[] = [
  3283. // Three winters of readings: the forecast has real data to work from.
  3284. {
  3285. vehicle: 0, customer: 1, season: "winter",
  3286. brand: "Nokian", model: "Hakkapeliitta R5", size: "235/45R18", dotCode: "3623",
  3287. loadSpeedIndex: "98T", withRims: true, rimType: 'Alloy, 18" OEM', hasTpms: true,
  3288. location: "A-1-1-L", storedDaysAgo: 118,
  3289. rounds: [[4.6, 4.4, 5.1, 5.0], [6.0, 5.9, 6.4, 6.3], [7.4, 7.3, 7.7, 7.6]],
  3290. treatments: { wash_rims: "done", balance: "done" },
  3291. notes: "Fronts wearing faster than the rears, as they always do on this car.",
  3292. },
  3293. {
  3294. vehicle: 4, customer: 3, season: "winter",
  3295. brand: "Continental", model: "WinterContact TS 870 P", size: "225/45R18", dotCode: "1024",
  3296. loadSpeedIndex: "95V", withRims: true, rimType: 'Alloy, 18" winter set', hasTpms: true,
  3297. location: "A-1-1-L", storedDaysAgo: 116,
  3298. rounds: [[5.2, 5.3, 5.8, 5.9], [6.5, 6.6, 6.9, 7.0], [7.8, 7.9, 8.1, 8.2]],
  3299. treatments: { wash_tires: "done", wash_rims: "done" },
  3300. },
  3301. // Down to the limit. Turns up on the forecast as needing replacing before
  3302. // the next season, which is the whole point of keeping the history.
  3303. {
  3304. vehicle: 5, customer: 9, season: "winter",
  3305. brand: "Bridgestone", model: "Blizzak WS90", size: "235/40R18", dotCode: "4122",
  3306. loadSpeedIndex: "95H", withRims: false,
  3307. location: "A-1-1-R", storedDaysAgo: 121,
  3308. rounds: [[3.1, 3.0, 3.4, 3.3], [4.6, 4.5, 4.8, 4.7], [6.1, 6.0, 6.3, 6.2]],
  3309. treatments: { wash_tires: "done", balance: "pending" },
  3310. notes: "Told the owner at check-in these will not pass another winter.",
  3311. },
  3312. {
  3313. vehicle: 6, customer: 5, season: "winter",
  3314. brand: "Michelin", model: "X-Ice Snow", size: "235/45R19", dotCode: "0824",
  3315. loadSpeedIndex: "99H", withRims: true, rimType: 'Alloy, 19" aero', hasTpms: true,
  3316. location: "A-1-1-R", storedDaysAgo: 110,
  3317. rounds: [[6.3, 6.4, 6.6, 6.7], [7.6, 7.7, 7.8, 7.9]],
  3318. treatments: { wash_rims: "done", tpms_service: "done" },
  3319. notes: "Aero covers in the boot with the tires. Do not fit without them.",
  3320. },
  3321. {
  3322. vehicle: 7, customer: 13, season: "winter",
  3323. brand: "Pirelli", model: "Scorpion Winter 2", size: "255/45R19", dotCode: "1525",
  3324. loadSpeedIndex: "104V", withRims: true, rimType: 'Alloy, 19" Gemini', hasTpms: true,
  3325. location: "A-1-2-L", storedDaysAgo: 104,
  3326. rounds: [[7.4, 7.5, 7.6, 7.6]],
  3327. treatments: { wash_rims: "done" },
  3328. notes: "First season with us. One year of readings, so no wear rate yet.",
  3329. },
  3330. {
  3331. vehicle: 10, customer: 7, season: "winter",
  3332. brand: "Vredestein", model: "Wintrac Pro", size: "245/40R18", dotCode: "3722",
  3333. loadSpeedIndex: "97V", withRims: false,
  3334. location: "A-1-2-L", storedDaysAgo: 126,
  3335. rounds: [[4.9, 4.8, 5.5, 5.4], [6.2, 6.1, 6.7, 6.6], [7.5, 7.4, 7.9, 7.8]],
  3336. treatments: { wash_tires: "done", balance: "done", new_valves: "done" },
  3337. },
  3338. {
  3339. vehicle: 11, customer: 11, season: "winter", studded: true,
  3340. brand: "Nokian", model: "Hakkapeliitta 10 SUV", size: "285/70R17", dotCode: "2823",
  3341. loadSpeedIndex: "121T", withRims: true, rimType: 'Steel, 17" black', hasTpms: true,
  3342. location: "A-1-2-R", storedDaysAgo: 133,
  3343. rounds: [[7.1, 7.0, 7.4, 7.3], [8.4, 8.3, 8.6, 8.5]],
  3344. treatments: { wash_tires: "done", repair: "done" },
  3345. notes: "Studded. Owner drives over the pass every weekend through winter.",
  3346. damage: "Sidewall scuff on the near-side rear, cosmetic only. Photographed at check-in.",
  3347. },
  3348. {
  3349. vehicle: 12, customer: 12, season: "winter",
  3350. brand: "Goodyear", model: "UltraGrip Performance +", size: "245/75R17", dotCode: "0524",
  3351. loadSpeedIndex: "121S", withRims: false,
  3352. location: "A-2-1-L", storedDaysAgo: 99,
  3353. rounds: [[6.6, 6.5, 6.9, 6.8], [8.0, 7.9, 8.2, 8.1]],
  3354. treatments: { wash_tires: "pending" },
  3355. },
  3356. {
  3357. vehicle: 13, customer: 3, season: "winter",
  3358. brand: "Michelin", model: "Pilot Alpin 5", size: "245/35R20", dotCode: "1124",
  3359. loadSpeedIndex: "95V", withRims: true, rimType: 'Alloy, staggered 20/21"', hasTpms: true,
  3360. location: "A-2-1-L", storedDaysAgo: 112,
  3361. rounds: [[5.8, 5.9, 5.1, 5.0], [7.0, 7.1, 6.4, 6.3]],
  3362. treatments: { wash_rims: "done", balance: "done", tpms_service: "done" },
  3363. notes: 'Staggered set: fronts 245/35R20, rears 305/30R21. Keep them paired.',
  3364. },
  3365. {
  3366. vehicle: 14, customer: 7, season: "winter",
  3367. brand: "Continental", model: "WinterContact TS 860", size: "225/40R18", dotCode: "3921",
  3368. loadSpeedIndex: "92H", withRims: true, rimType: 'Alloy, 18" Nogaro', hasTpms: true,
  3369. location: "A-2-1-R", storedDaysAgo: 129,
  3370. rounds: [[4.1, 4.0, 4.6, 4.5], [5.5, 5.4, 5.9, 5.8], [6.9, 6.8, 7.2, 7.1]],
  3371. treatments: { wash_tires: "done", wash_rims: "done", balance: "done" },
  3372. notes: "One more winter in them at this rate, then they need doing.",
  3373. },
  3374. {
  3375. vehicle: 15, customer: 17, season: "winter",
  3376. brand: "Bridgestone", model: "Blizzak DM-V2", size: "275/55R20", dotCode: "2224",
  3377. loadSpeedIndex: "117T", withRims: false, quantity: 5, pressureBar: 2.8,
  3378. location: "B-1-1-L", storedDaysAgo: 95,
  3379. rounds: [[7.8, 7.7, 8.0, 7.9]],
  3380. treatments: { wash_tires: "done" },
  3381. notes: "Five tires: four plus a matching full-size spare.",
  3382. },
  3383. {
  3384. vehicle: 16, customer: 19, season: "winter",
  3385. brand: "Toyo", model: "Observe GSi-6", size: "265/70R16", dotCode: "1723",
  3386. loadSpeedIndex: "112H", withRims: true, rimType: 'Steel, 16" with trims',
  3387. location: "B-1-1-L", storedDaysAgo: 138,
  3388. rounds: [[6.0, 6.1, 6.5, 6.4], [7.3, 7.4, 7.6, 7.5]],
  3389. treatments: { wash_tires: "done", new_valves: "done" },
  3390. },
  3391. {
  3392. vehicle: 1, customer: 0, season: "winter",
  3393. brand: "Firestone", model: "Winterforce 2 UV", size: "275/60R20", dotCode: "3123",
  3394. loadSpeedIndex: "115S", withRims: true, rimType: 'Steel, 20" fleet spec', hasTpms: true,
  3395. pressureBar: 2.8,
  3396. location: "B-1-1-R", storedDaysAgo: 141,
  3397. rounds: [[5.4, 5.3, 5.9, 5.8], [6.8, 6.7, 7.1, 7.0], [8.1, 8.0, 8.3, 8.2]],
  3398. treatments: { wash_tires: "done", balance: "done" },
  3399. notes: "Fleet vehicle. Invoice goes on the Summit Construction account.",
  3400. },
  3401. {
  3402. vehicle: 2, customer: 11, season: "all_season",
  3403. brand: "Michelin", model: "CrossClimate 2", size: "265/70R17", dotCode: "0225",
  3404. loadSpeedIndex: "115T", withRims: false,
  3405. location: "B-1-2-L", storedDaysAgo: 87,
  3406. rounds: [[6.7, 6.6, 7.0, 6.9]],
  3407. treatments: { wash_tires: "pending", balance: "pending" },
  3408. notes: "Second vehicle, so the all-seasons come off over the summer too.",
  3409. },
  3410. // Collected. Off the shelf, still in the history.
  3411. {
  3412. vehicle: 3, customer: 4, season: "winter",
  3413. brand: "Hankook", model: "Winter i*cept evo3 X", size: "275/55R20", dotCode: "4023",
  3414. loadSpeedIndex: "117V", withRims: true, rimType: 'Alloy, 20" platinum', hasTpms: true,
  3415. location: null, lastShelf: "B-1-1-R", status: "released", storedDaysAgo: 300, goneDaysAgo: 21,
  3416. rounds: [[6.2, 6.3, 6.7, 6.6], [7.5, 7.6, 7.8, 7.7]],
  3417. treatments: { wash_tires: "done", wash_rims: "done", balance: "done" },
  3418. notes: "Collected when the fleet contract moved depot.",
  3419. },
  3420. // Written off at the limit. Shows what a disposal looks like afterwards.
  3421. {
  3422. vehicle: 9, customer: 15, season: "winter",
  3423. brand: "Semperit", model: "Van-Grip 3", size: "235/65R16C", dotCode: "1219",
  3424. loadSpeedIndex: "115R", withRims: false,
  3425. location: null, lastShelf: "B-1-2-L", status: "disposed", storedDaysAgo: 240, goneDaysAgo: 34,
  3426. pressureBar: 3.2,
  3427. rounds: [[2.2, 2.1, 2.6, 2.5], [3.9, 3.8, 4.2, 4.1]],
  3428. notes: "Scrapped with the owner's agreement. New set fitted the same day.",
  3429. },
  3430. ];
  3431. /** Roughly a season between a set going out and coming back. */
  3432. const SEASON_DAYS = 182;
  3433. /** A whole year between one check-in and the next. */
  3434. const YEAR_DAYS = 365;
  3435. let tireReference = 0;
  3436. const tireSetIds: string[] = [];
  3437. for (const spec of TIRE_SETS) {
  3438. tireReference += 1;
  3439. const status = spec.status ?? "stored";
  3440. const quantity = spec.quantity ?? 4;
  3441. const location = spec.location ? shelf.get(spec.location) : undefined;
  3442. // Where the movement trail says it went. A set that has been collected
  3443. // still spent its winters on a real shelf, and the history should say so.
  3444. const historyShelf = shelf.get(spec.location ?? spec.lastShelf ?? "");
  3445. const checkedInAt = days(-spec.storedDaysAgo);
  3446. const checkedOutAt = spec.goneDaysAgo ? days(-spec.goneDaysAgo) : null;
  3447. const set = await prisma.tireSet.create({
  3448. data: {
  3449. reference: String(tireReference),
  3450. season: spec.season,
  3451. studded: spec.studded ?? false,
  3452. brand: spec.brand,
  3453. model: spec.model,
  3454. size: spec.size,
  3455. dotCode: spec.dotCode,
  3456. loadSpeedIndex: spec.loadSpeedIndex ?? null,
  3457. withRims: spec.withRims,
  3458. rimType: spec.rimType ?? null,
  3459. hasTpms: spec.hasTpms ?? false,
  3460. quantity,
  3461. status,
  3462. notes: spec.notes ?? null,
  3463. locationId: location?.id ?? null,
  3464. vehicleId: vehicles[spec.vehicle].id,
  3465. customerId: customers[spec.customer].id,
  3466. organizationId: ORG_ID,
  3467. userId: USER_ID,
  3468. checkedInAt,
  3469. checkedOutAt,
  3470. },
  3471. });
  3472. tireSetIds.push(set.id);
  3473. // Oldest first, so the movement trail reads forwards: arrived, left,
  3474. // arrived again. Each round is one check-in, a year apart.
  3475. for (let i = spec.rounds.length - 1; i >= 0; i -= 1) {
  3476. const arrivedAt = days(-(spec.storedDaysAgo + i * YEAR_DAYS));
  3477. const movement = await prisma.tireMovement.create({
  3478. data: {
  3479. type: "check_in",
  3480. toCode: historyShelf?.code ?? null,
  3481. toLocationId: historyShelf?.id ?? null,
  3482. note: i === spec.rounds.length - 1 ? "First check-in for this set." : null,
  3483. tireSetId: set.id,
  3484. organizationId: ORG_ID,
  3485. performedById: USER_ID,
  3486. createdAt: arrivedAt,
  3487. },
  3488. });
  3489. await prisma.tireMeasurement.createMany({
  3490. data: TIRE_ROAD_POSITIONS.map((position, index) => ({
  3491. position,
  3492. treadDepthMm: spec.rounds[i][index],
  3493. pressureBar: spec.pressureBar ?? 2.4,
  3494. // Left as the grade the reading implies at the shop's own limits.
  3495. // The app derives what it shows from the depth, so this is only a
  3496. // fallback for a reading taken by eye.
  3497. condition: gradeTread(spec.rounds[i][index], spec.season, DEMO_TREAD_LIMITS) ?? "good",
  3498. damage: i === 0 && position === "rear_left" ? (spec.damage ?? null) : null,
  3499. tireSetId: set.id,
  3500. movementId: movement.id,
  3501. measuredById: USER_ID,
  3502. measuredAt: arrivedAt,
  3503. })),
  3504. });
  3505. // Everything except the most recent round went back on the car for a
  3506. // season before coming in again.
  3507. if (i > 0) {
  3508. await prisma.tireMovement.create({
  3509. data: {
  3510. type: "check_out",
  3511. fromCode: historyShelf?.code ?? null,
  3512. fromLocationId: historyShelf?.id ?? null,
  3513. note: "Refitted for the season.",
  3514. tireSetId: set.id,
  3515. organizationId: ORG_ID,
  3516. performedById: USER_ID,
  3517. createdAt: days(-(spec.storedDaysAgo + i * YEAR_DAYS - SEASON_DAYS)),
  3518. },
  3519. });
  3520. }
  3521. }
  3522. if (status !== "stored" && spec.goneDaysAgo) {
  3523. await prisma.tireMovement.create({
  3524. data: {
  3525. type: status === "disposed" ? "dispose" : "check_out",
  3526. fromCode: historyShelf?.code ?? null,
  3527. fromLocationId: historyShelf?.id ?? null,
  3528. note:
  3529. status === "disposed"
  3530. ? "Written off at the tread limit, with the owner's agreement."
  3531. : "Collected by the customer.",
  3532. tireSetId: set.id,
  3533. organizationId: ORG_ID,
  3534. performedById: USER_ID,
  3535. createdAt: days(-spec.goneDaysAgo),
  3536. },
  3537. });
  3538. }
  3539. if (spec.treatments) {
  3540. await prisma.tireTreatment.createMany({
  3541. data: Object.entries(spec.treatments).map(([type, treatmentStatus]) => ({
  3542. type,
  3543. status: treatmentStatus,
  3544. tireSetId: set.id,
  3545. organizationId: ORG_ID,
  3546. completedAt: treatmentStatus === "done" ? checkedInAt : null,
  3547. completedById: treatmentStatus === "done" ? USER_ID : null,
  3548. })),
  3549. });
  3550. }
  3551. }
  3552. // One set moved between shelves, so the history shows a relocation and not
  3553. // just arrivals and departures.
  3554. await prisma.tireMovement.create({
  3555. data: {
  3556. type: "relocate",
  3557. fromCode: "A-2-1-R",
  3558. toCode: "A-1-2-R",
  3559. fromLocationId: shelf.get("A-2-1-R")?.id ?? null,
  3560. toLocationId: shelf.get("A-1-2-R")?.id ?? null,
  3561. note: "Moved off the top shelf, owner collects these himself.",
  3562. tireSetId: tireSetIds[6],
  3563. organizationId: ORG_ID,
  3564. performedById: USER_ID,
  3565. createdAt: days(-40),
  3566. },
  3567. });
  3568. console.log(
  3569. ` Created ${tireLocations.length} shelves and ${TIRE_SETS.length} tire sets`,
  3570. );
  3571. // -- Audit log --
  3572. // Written by the app in normal use; seeded here so the page is not blank.
  3573. console.log("\nCreating audit log entries...");
  3574. const auditEntries: { action: string; entity: string; entityId: string; message: string; day: number; hour: number }[] = [
  3575. { action: "serviceRecord.create", entity: "serviceRecord", entityId: boardServiceRecords[0].id, message: "Created work order \"Honda Civic - AC Compressor\"", day: MONDAY_OFFSET, hour: 7 },
  3576. { action: "serviceRecord.update", entity: "serviceRecord", entityId: boardServiceRecords[6].id, message: "Moved \"CAT 745 - Transmission Rebuild\" to in progress", day: MONDAY_OFFSET, hour: 8 },
  3577. { action: "quote.send", entity: "quote", entityId: quotes[0].id, message: "Sent quote to Sarah Coleman", day: -6, hour: 14 },
  3578. { action: "quote.accept", entity: "quote", entityId: quotes[10].id, message: "Quote accepted by Mike Thompson", day: -2, hour: 11 },
  3579. { action: "payment.create", entity: "serviceRecord", entityId: serviceRecords[4].id, message: "Recorded a payment against the Kenworth engine service", day: -3, hour: 16 },
  3580. { action: "inventoryPart.update", entity: "inventoryPart", entityId: inventoryParts[21].id, message: "Adjusted stock for DPF Pressure Sensor", day: -7, hour: 10 },
  3581. { action: "inspection.complete", entity: "inspection", entityId: inspections[4].id, message: "Completed a periodic technical inspection on the Volvo FH 640", day: wd(-2, 3), hour: 11 },
  3582. { action: "inspection.complete", entity: "inspection", entityId: inspections[7].id, message: "Recorded a dangerous defect on the Mack Granite", day: wd(0, 0), hour: 12 },
  3583. { action: "customer.create", entity: "customer", entityId: customers[19].id, message: "Added customer Lisa Martinez", day: -34, hour: 9 },
  3584. { action: "vehicle.create", entity: "vehicle", entityId: vehicles[41].id, message: "Added vehicle Subaru WRX Premium", day: -34, hour: 9 },
  3585. { action: "reminder.create", entity: "reminder", entityId: coreReminders[0].id, message: "Created reminder \"Annual DOT Inspection\"", day: -30, hour: 15 },
  3586. { action: "scheduledMessage.create", entity: "scheduledMessage", entityId: scheduledMessages[12].id, message: "Scheduled a monthly fleet statement for Summit Construction", day: -24, hour: 8 },
  3587. { action: "settings.update", entity: "appSetting", entityId: ORG_ID, message: "Updated invoice layout settings", day: -12, hour: 13 },
  3588. { action: "laborPreset.create", entity: "laborPreset", entityId: laborPresets[2].id, message: "Created labor preset \"Heavy truck PM service\"", day: -18, hour: 10 },
  3589. { action: "technician.create", entity: "technician", entityId: technicians[9].id, message: "Added technician Daniel Eriksen", day: -60, hour: 9 },
  3590. { action: "user.login", entity: "user", entityId: USER_ID, message: "Signed in", day: 0, hour: 7 },
  3591. ];
  3592. await Promise.all(
  3593. auditEntries.map((e) =>
  3594. prisma.auditLog.create({
  3595. data: {
  3596. organizationId: ORG_ID,
  3597. userId: USER_ID,
  3598. action: e.action,
  3599. entity: e.entity,
  3600. entityId: e.entityId,
  3601. message: e.message,
  3602. timestamp: at(e.day, e.hour),
  3603. ip: "203.0.113.42",
  3604. userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/140.0 Safari/537.36",
  3605. },
  3606. }),
  3607. ),
  3608. );
  3609. console.log(` Created ${auditEntries.length} audit log entries`);
  3610. // -- Summary --
  3611. console.log("\n" + "=".repeat(50));
  3612. console.log("Seed completed!");
  3613. console.log("=".repeat(50));
  3614. console.log(` Seed date: ${TODAY.toDateString()} (board week starts ${weekMonday().toDateString()})`);
  3615. console.log(` Customers: ${customers.length}`);
  3616. console.log(` Vehicles: ${vehicles.length}`);
  3617. console.log(` Service Records: ${serviceRecords.length + boardServiceRecords.length + unassignedRecords.length + predMaintRecords.length + spreadRecords.length}`);
  3618. console.log(` Invoices / paid: ${completed.length} / ${paidFull + paidPartial}`);
  3619. console.log(` Quotes: ${quotes.length}`);
  3620. console.log(` Inventory Parts: ${inventoryParts.length}`);
  3621. console.log(` Stock Movements: ${movementCount}`);
  3622. console.log(` Technicians: ${technicians.length}`);
  3623. console.log(` Unassigned Jobs: ${unassignedRecords.length}`);
  3624. console.log(` Notes: ${9 + additionalNotes.length}`);
  3625. console.log(` Reminders: ${coreReminders.length + additionalReminders.length}`);
  3626. console.log(` Findings: ${findings.length}`);
  3627. console.log(` Inspections: ${inspections.length} (${templates.length} templates)`);
  3628. console.log(` Condition Marks: ${marks.length} (${photoCount} photos)`);
  3629. console.log(` Work Bays: ${bays.length}`);
  3630. console.log(` Custom Statuses: ${statusRows.length}`);
  3631. console.log(` Concerns: ${concerns.length}`);
  3632. console.log(` Time Entries: ${clocks.length}`);
  3633. console.log(` SMS Messages: 22`);
  3634. console.log(` WhatsApp Messages: ${whatsapp.length}`);
  3635. console.log(` Telegram Messages: ${telegram.length}`);
  3636. console.log(` Scheduled Messages: ${scheduledMessages.length}`);
  3637. console.log(` Notifications: ${notifications.length}`);
  3638. console.log(` Labor Presets: ${laborPresets.length}`);
  3639. console.log(` Roles: ${roles.length}`);
  3640. console.log(` Webhooks: ${webhooks.length} (inactive)`);
  3641. console.log(` Report Schedules: ${reportSchedules.length}`);
  3642. console.log(` Recurring Invoices: ${recurring.length}`);
  3643. console.log(` Custom Fields: ${customFields.length}`);
  3644. console.log(` Service Requests: ${serviceRequests.length}`);
  3645. console.log(` Status Reports: ${statusReports.length}`);
  3646. console.log(` Audit Entries: ${auditEntries.length}`);
  3647. console.log("=".repeat(50));
  3648. }
  3649. seed()
  3650. .then(async () => { await prisma.$disconnect(); process.exit(0); })
  3651. .catch(async (e) => { console.error("Seed failed:", e); await prisma.$disconnect(); process.exit(1); });