| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349 |
- import { db } from '@/lib/db'
- import { resolveInvoicePrefix } from '@/lib/invoice-utils'
- import type { SampleDataIds } from './onboardingKeys'
- import type { Translator } from './onboardingDefaults'
- /** The slice of an inspection template the seed copies items from. */
- interface TemplateForSeed {
- id: string
- severityScale: string
- country: string | null
- sections: {
- name: string
- code: string | null
- items: {
- name: string
- description: string | null
- code: string | null
- sortOrder: number
- inputType: string
- unit: string | null
- minValue: number | null
- maxValue: number | null
- choices: string[]
- required: boolean
- photoRequired: boolean
- defaultSeverity: string | null
- defectSuggestions: string[]
- }[]
- }[]
- }
- const daysAgo = (n: number) => {
- const d = new Date()
- d.setDate(d.getDate() - n)
- return d
- }
- const daysAhead = (n: number) => daysAgo(-n)
- /**
- * Seeds a small, clearly-labelled demo dataset for a brand-new organization:
- * three customers, four vehicles, three work orders in different states (one
- * completed with parts and labor), one sent quote and one inspection in
- * progress. Every name carries the translated sample prefix, and every
- * created id is returned so removal can delete exactly this set.
- *
- * Invoice and quote numbers follow the same default numbering the real
- * actions use, so real records created afterwards simply continue the
- * sequence.
- */
- export async function seedSampleData(
- organizationId: string,
- userId: string,
- t: Translator,
- template: TemplateForSeed
- ): Promise<SampleDataIds> {
- const prefix = t('sample.prefix')
- const label = (name: string) => `${prefix} ${name}`
- const invoicePrefix = resolveInvoicePrefix('{year}-')
- const hourlyRate = 95
- return db.$transaction(async (tx) => {
- // ── Customers ────────────────────────────────────────────────────────
- const anna = await tx.customer.create({
- data: {
- name: label('Anna Bergström'),
- customerNumber: '1',
- email: 'anna.sample@example.com',
- phone: '+1 555 0101',
- userId,
- organizationId,
- },
- })
- const marco = await tx.customer.create({
- data: {
- name: label('Marco Rossi'),
- customerNumber: '2',
- email: 'marco.sample@example.com',
- phone: '+1 555 0102',
- userId,
- organizationId,
- },
- })
- const nordvik = await tx.customer.create({
- data: {
- name: label('Nordvik Logistics'),
- customerNumber: '3',
- company: label('Nordvik Logistics'),
- email: 'fleet.sample@example.com',
- phone: '+1 555 0103',
- userId,
- organizationId,
- },
- })
- // ── Vehicles ─────────────────────────────────────────────────────────
- const golf = await tx.vehicle.create({
- data: {
- make: 'Volkswagen',
- model: 'Golf',
- year: 2018,
- licensePlate: 'SAMPLE-1',
- mileage: 86500,
- fuelType: 'gasoline',
- customerId: anna.id,
- userId,
- organizationId,
- },
- })
- const rav4 = await tx.vehicle.create({
- data: {
- make: 'Toyota',
- model: 'RAV4',
- year: 2020,
- licensePlate: 'SAMPLE-2',
- mileage: 45200,
- fuelType: 'hybrid',
- customerId: marco.id,
- userId,
- organizationId,
- },
- })
- const transit = await tx.vehicle.create({
- data: {
- make: 'Ford',
- model: 'Transit',
- year: 2016,
- licensePlate: 'SAMPLE-3',
- mileage: 158000,
- fuelType: 'diesel',
- customerId: nordvik.id,
- userId,
- organizationId,
- },
- })
- const bmw = await tx.vehicle.create({
- data: {
- make: 'BMW',
- model: '320d',
- year: 2019,
- licensePlate: 'SAMPLE-4',
- mileage: 98400,
- fuelType: 'diesel',
- customerId: anna.id,
- userId,
- organizationId,
- },
- })
- // ── Work orders ──────────────────────────────────────────────────────
- // Completed job with parts + labor, invoiced and paid.
- const oilParts = [
- { name: t('sample.parts.oilFilter'), quantity: 1, unitPrice: 14.9, total: 14.9 },
- { name: t('sample.parts.engineOil'), quantity: 4, unit: 'l', unitPrice: 11.5, total: 46 },
- ]
- const oilLaborTotal = 0.8 * hourlyRate
- const oilSubtotal = oilParts.reduce((sum, p) => sum + p.total, 0) + oilLaborTotal
- const oilJob = await tx.serviceRecord.create({
- data: {
- title: label(t('sample.oilService')),
- type: 'maintenance',
- status: 'completed',
- mileage: 86200,
- serviceDate: daysAgo(12),
- startDateTime: daysAgo(12),
- invoiceDate: daysAgo(12),
- invoiceNumber: `${invoicePrefix}1001`,
- subtotal: oilSubtotal,
- totalAmount: oilSubtotal,
- cost: oilSubtotal,
- manuallyPaid: true,
- vehicleId: golf.id,
- organizationId,
- partItems: { create: oilParts },
- laborItems: {
- create: [
- {
- description: t('sample.oilService'),
- hours: 0.8,
- rate: hourlyRate,
- total: oilLaborTotal,
- pricingType: 'hourly',
- },
- ],
- },
- },
- })
- // Job on the lift right now.
- const brakeLaborTotal = 1.5 * hourlyRate
- const brakeSubtotal = 62 + brakeLaborTotal
- const brakeJob = await tx.serviceRecord.create({
- data: {
- title: label(t('sample.brakeJob')),
- type: 'repair',
- status: 'in-progress',
- mileage: 45200,
- serviceDate: new Date(),
- startDateTime: new Date(),
- invoiceNumber: `${invoicePrefix}1002`,
- subtotal: brakeSubtotal,
- totalAmount: brakeSubtotal,
- cost: brakeSubtotal,
- vehicleId: rav4.id,
- organizationId,
- partItems: {
- create: [
- {
- name: t('sample.parts.brakePads'),
- quantity: 1,
- unitPrice: 62,
- total: 62,
- },
- ],
- },
- laborItems: {
- create: [
- {
- description: t('sample.brakeJob'),
- hours: 1.5,
- rate: hourlyRate,
- total: brakeLaborTotal,
- pricingType: 'hourly',
- },
- ],
- },
- },
- })
- // Booked but not started.
- const annualJob = await tx.serviceRecord.create({
- data: {
- title: label(t('sample.annualService')),
- type: 'maintenance',
- status: 'pending',
- serviceDate: daysAhead(3),
- startDateTime: daysAhead(3),
- invoiceNumber: `${invoicePrefix}1003`,
- vehicleId: transit.id,
- organizationId,
- },
- })
- // ── Quote ────────────────────────────────────────────────────────────
- const quoteLaborTotal = 2 * hourlyRate
- const quoteSubtotal = 178 + quoteLaborTotal
- const quote = await tx.quote.create({
- data: {
- quoteNumber: 'QT-1001',
- title: label(t('sample.quoteTitle')),
- status: 'sent',
- validUntil: daysAhead(30),
- subtotal: quoteSubtotal,
- totalAmount: quoteSubtotal,
- customerId: anna.id,
- vehicleId: bmw.id,
- userId,
- organizationId,
- partItems: {
- create: [
- {
- name: t('sample.parts.shockAbsorber'),
- quantity: 2,
- unitPrice: 89,
- total: 178,
- },
- ],
- },
- laborItems: {
- create: [
- {
- description: t('sample.quoteTitle'),
- hours: 2,
- rate: hourlyRate,
- total: quoteLaborTotal,
- pricingType: 'hourly',
- },
- ],
- },
- },
- })
- // ── Inspection ───────────────────────────────────────────────────────
- // Items are copied from the installed default template exactly like
- // createInspection does; the first few carry results so the inspection
- // screen shows what a partially-done checklist looks like.
- const inspection = await tx.inspection.create({
- data: {
- status: 'in_progress',
- mileage: 86500,
- severityScale: template.severityScale,
- country: template.country,
- vehicleId: golf.id,
- templateId: template.id,
- organizationId,
- },
- })
- let inspected = 0
- const items = template.sections.flatMap((section, sIdx) =>
- section.items.map((item) => {
- // Grade the first handful of plain condition checks, leave the rest
- // untouched; one gets flagged so the attention state is visible too.
- let condition = 'not_inspected'
- let notes: string | null = null
- if (item.inputType === 'condition' && inspected < 5) {
- inspected += 1
- if (inspected === 4) {
- condition = 'attention'
- notes = t('sample.inspectionNote')
- } else {
- condition = 'pass'
- }
- }
- return {
- name: item.name,
- section: section.name,
- sectionCode: section.code,
- description: item.description,
- code: item.code,
- sortOrder: sIdx * 1000 + item.sortOrder,
- inputType: item.inputType,
- unit: item.unit,
- minValue: item.minValue,
- maxValue: item.maxValue,
- choices: item.choices,
- required: item.required,
- photoRequired: item.photoRequired,
- defaultSeverity: item.defaultSeverity,
- defectSuggestions: item.defectSuggestions,
- condition,
- notes,
- inspectionId: inspection.id,
- }
- })
- )
- if (items.length > 0) {
- await tx.inspectionItem.createMany({ data: items })
- }
- return {
- customers: [anna.id, marco.id, nordvik.id],
- vehicles: [golf.id, rav4.id, transit.id, bmw.id],
- serviceRecords: [oilJob.id, brakeJob.id, annualJob.id],
- quotes: [quote.id],
- inspections: [inspection.id],
- }
- })
- }
|