This commit is contained in:
@@ -0,0 +1,232 @@
|
|||||||
|
import { createHash } from 'node:crypto'
|
||||||
|
import { Prisma } from '@prisma/client'
|
||||||
|
import { prisma } from './db.js'
|
||||||
|
|
||||||
|
interface DemoWorkspaceAccess {
|
||||||
|
teamUuid: string
|
||||||
|
userId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DemoRoute {
|
||||||
|
title: string
|
||||||
|
sourceCountryCode: string
|
||||||
|
sourceLocationName: string
|
||||||
|
sourceLatitude: number
|
||||||
|
sourceLongitude: number
|
||||||
|
destinationCountryCode: string
|
||||||
|
destinationLocationName: string
|
||||||
|
destinationLatitude: number
|
||||||
|
destinationLongitude: number
|
||||||
|
transportTypeCode: string
|
||||||
|
amountUsd: number
|
||||||
|
etaDays: number
|
||||||
|
grossWeightKg: number
|
||||||
|
volumeCbm: number
|
||||||
|
routeDistanceKm: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const demoRoutes: DemoRoute[] = [
|
||||||
|
{
|
||||||
|
title: 'Mombasa → Almaty',
|
||||||
|
sourceCountryCode: 'KE',
|
||||||
|
sourceLocationName: 'Mombasa',
|
||||||
|
sourceLatitude: -4.0435,
|
||||||
|
sourceLongitude: 39.6682,
|
||||||
|
destinationCountryCode: 'KZ',
|
||||||
|
destinationLocationName: 'Almaty',
|
||||||
|
destinationLatitude: 43.2389,
|
||||||
|
destinationLongitude: 76.8897,
|
||||||
|
transportTypeCode: 'MULTIMODAL',
|
||||||
|
amountUsd: 2450,
|
||||||
|
etaDays: 16,
|
||||||
|
grossWeightKg: 18400,
|
||||||
|
volumeCbm: 68,
|
||||||
|
routeDistanceKm: 8240,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Casablanca → Moscow',
|
||||||
|
sourceCountryCode: 'MA',
|
||||||
|
sourceLocationName: 'Casablanca',
|
||||||
|
sourceLatitude: 33.5731,
|
||||||
|
sourceLongitude: -7.5898,
|
||||||
|
destinationCountryCode: 'RU',
|
||||||
|
destinationLocationName: 'Moscow',
|
||||||
|
destinationLatitude: 55.7558,
|
||||||
|
destinationLongitude: 37.6173,
|
||||||
|
transportTypeCode: 'MULTIMODAL',
|
||||||
|
amountUsd: 3100,
|
||||||
|
etaDays: 12,
|
||||||
|
grossWeightKg: 12600,
|
||||||
|
volumeCbm: 52,
|
||||||
|
routeDistanceKm: 5060,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Nairobi → Dubai',
|
||||||
|
sourceCountryCode: 'KE',
|
||||||
|
sourceLocationName: 'Nairobi',
|
||||||
|
sourceLatitude: -1.2921,
|
||||||
|
sourceLongitude: 36.8219,
|
||||||
|
destinationCountryCode: 'AE',
|
||||||
|
destinationLocationName: 'Dubai',
|
||||||
|
destinationLatitude: 25.2048,
|
||||||
|
destinationLongitude: 55.2708,
|
||||||
|
transportTypeCode: 'AIR',
|
||||||
|
amountUsd: 1850,
|
||||||
|
etaDays: 4,
|
||||||
|
grossWeightKg: 4200,
|
||||||
|
volumeCbm: 18,
|
||||||
|
routeDistanceKm: 3560,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Johannesburg → Warsaw',
|
||||||
|
sourceCountryCode: 'ZA',
|
||||||
|
sourceLocationName: 'Johannesburg',
|
||||||
|
sourceLatitude: -26.2041,
|
||||||
|
sourceLongitude: 28.0473,
|
||||||
|
destinationCountryCode: 'PL',
|
||||||
|
destinationLocationName: 'Warsaw',
|
||||||
|
destinationLatitude: 52.2297,
|
||||||
|
destinationLongitude: 21.0122,
|
||||||
|
transportTypeCode: 'AIR',
|
||||||
|
amountUsd: 4200,
|
||||||
|
etaDays: 7,
|
||||||
|
grossWeightKg: 7600,
|
||||||
|
volumeCbm: 31,
|
||||||
|
routeDistanceKm: 8690,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const demoNote = JSON.stringify({ seed: 'empty-team-workspace' })
|
||||||
|
|
||||||
|
function stableUuid(teamUuid: string, entity: string, index: number): string {
|
||||||
|
const hex = createHash('sha256')
|
||||||
|
.update(`${teamUuid}:${entity}:${index}`)
|
||||||
|
.digest('hex')
|
||||||
|
.slice(0, 32)
|
||||||
|
.split('')
|
||||||
|
hex[12] = '4'
|
||||||
|
hex[16] = '8'
|
||||||
|
const value = hex.join('')
|
||||||
|
return `${value.slice(0, 8)}-${value.slice(8, 12)}-${value.slice(12, 16)}-${value.slice(16, 20)}-${value.slice(20)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureDemoWorkspaceData(access: DemoWorkspaceAccess): Promise<void> {
|
||||||
|
const [quotationCount, orderCount] = await Promise.all([
|
||||||
|
prisma.quotation.count({ where: { teamUuid: access.teamUuid } }),
|
||||||
|
prisma.order.count({ where: { teamUuid: access.teamUuid } }),
|
||||||
|
])
|
||||||
|
const needsCalculations = quotationCount === 0
|
||||||
|
const needsOrders = orderCount === 0
|
||||||
|
if (!needsCalculations && !needsOrders) return
|
||||||
|
|
||||||
|
for (const [index, route] of demoRoutes.entries()) {
|
||||||
|
const createsOrder = index < 2
|
||||||
|
if (!needsCalculations && !(needsOrders && createsOrder)) continue
|
||||||
|
|
||||||
|
const tariffUuid = stableUuid(access.teamUuid, 'demo-tariff', index)
|
||||||
|
const tariff = await prisma.tariffReference.upsert({
|
||||||
|
where: { uuid: tariffUuid },
|
||||||
|
create: {
|
||||||
|
uuid: tariffUuid,
|
||||||
|
teamUuid: access.teamUuid,
|
||||||
|
createdByUserId: access.userId,
|
||||||
|
name: route.title,
|
||||||
|
status: 'active',
|
||||||
|
operationCode: 'IMPORT',
|
||||||
|
transportTypeCode: route.transportTypeCode,
|
||||||
|
sourceCountryCode: route.sourceCountryCode,
|
||||||
|
destinationCountryCode: route.destinationCountryCode,
|
||||||
|
amountUsd: new Prisma.Decimal(route.amountUsd),
|
||||||
|
minPriceUsd: new Prisma.Decimal(route.amountUsd),
|
||||||
|
etaDays: route.etaDays,
|
||||||
|
priority: 50 + index,
|
||||||
|
currency: 'USD',
|
||||||
|
notes: demoNote,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
teamUuid: access.teamUuid,
|
||||||
|
createdByUserId: access.userId,
|
||||||
|
amountUsd: new Prisma.Decimal(route.amountUsd),
|
||||||
|
minPriceUsd: new Prisma.Decimal(route.amountUsd),
|
||||||
|
etaDays: route.etaDays,
|
||||||
|
notes: demoNote,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const quotationUuid = stableUuid(access.teamUuid, 'demo-quotation', index)
|
||||||
|
const quotation = await prisma.quotation.upsert({
|
||||||
|
where: { uuid: quotationUuid },
|
||||||
|
create: {
|
||||||
|
uuid: quotationUuid,
|
||||||
|
teamUuid: access.teamUuid,
|
||||||
|
createdByUserId: access.userId,
|
||||||
|
title: route.title,
|
||||||
|
status: createsOrder && needsOrders ? 'converted' : 'priced',
|
||||||
|
operationCode: 'IMPORT',
|
||||||
|
incotermsCode: 'DAP',
|
||||||
|
transportTypeCode: route.transportTypeCode,
|
||||||
|
sourceCountryCode: route.sourceCountryCode,
|
||||||
|
sourceLocationName: route.sourceLocationName,
|
||||||
|
sourceLatitude: route.sourceLatitude,
|
||||||
|
sourceLongitude: route.sourceLongitude,
|
||||||
|
destinationCountryCode: route.destinationCountryCode,
|
||||||
|
destinationLocationName: route.destinationLocationName,
|
||||||
|
destinationLatitude: route.destinationLatitude,
|
||||||
|
destinationLongitude: route.destinationLongitude,
|
||||||
|
grossWeightKg: new Prisma.Decimal(route.grossWeightKg),
|
||||||
|
chargeableWeightKg: new Prisma.Decimal(route.grossWeightKg),
|
||||||
|
volumeCbm: new Prisma.Decimal(route.volumeCbm),
|
||||||
|
unitsCount: 1 + index,
|
||||||
|
routeDistanceKm: route.routeDistanceKm,
|
||||||
|
selectedTariffId: tariff.id,
|
||||||
|
tariffMatchSummary: `Demo tariff: ${route.title}`,
|
||||||
|
totalAmount: new Prisma.Decimal(route.amountUsd),
|
||||||
|
currency: 'USD',
|
||||||
|
etaDays: route.etaDays,
|
||||||
|
notes: demoNote,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
teamUuid: access.teamUuid,
|
||||||
|
createdByUserId: access.userId,
|
||||||
|
selectedTariffId: tariff.id,
|
||||||
|
totalAmount: new Prisma.Decimal(route.amountUsd),
|
||||||
|
currency: 'USD',
|
||||||
|
etaDays: route.etaDays,
|
||||||
|
notes: demoNote,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!needsOrders || !createsOrder) continue
|
||||||
|
await prisma.order.upsert({
|
||||||
|
where: { uuid: stableUuid(access.teamUuid, 'demo-order', index) },
|
||||||
|
create: {
|
||||||
|
uuid: stableUuid(access.teamUuid, 'demo-order', index),
|
||||||
|
teamUuid: access.teamUuid,
|
||||||
|
quotationId: quotation.id,
|
||||||
|
createdByUserId: access.userId,
|
||||||
|
name: route.title,
|
||||||
|
status: index === 0 ? 'in_transit' : 'processing',
|
||||||
|
totalAmount: new Prisma.Decimal(route.amountUsd),
|
||||||
|
currency: 'USD',
|
||||||
|
sourceLocationName: route.sourceLocationName,
|
||||||
|
sourceCountryCode: route.sourceCountryCode,
|
||||||
|
sourceLatitude: route.sourceLatitude,
|
||||||
|
sourceLongitude: route.sourceLongitude,
|
||||||
|
destinationLocationName: route.destinationLocationName,
|
||||||
|
destinationCountryCode: route.destinationCountryCode,
|
||||||
|
destinationLatitude: route.destinationLatitude,
|
||||||
|
destinationLongitude: route.destinationLongitude,
|
||||||
|
etaDays: route.etaDays,
|
||||||
|
notes: demoNote,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
teamUuid: access.teamUuid,
|
||||||
|
createdByUserId: access.userId,
|
||||||
|
status: index === 0 ? 'in_transit' : 'processing',
|
||||||
|
totalAmount: new Prisma.Decimal(route.amountUsd),
|
||||||
|
currency: 'USD',
|
||||||
|
notes: demoNote,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from '@prisma/client'
|
} from '@prisma/client'
|
||||||
import { requireScopes, type AuthContext } from '../auth.js'
|
import { requireScopes, type AuthContext } from '../auth.js'
|
||||||
import { prisma } from '../db.js'
|
import { prisma } from '../db.js'
|
||||||
|
import { ensureDemoWorkspaceData } from '../demo-workspace.js'
|
||||||
|
|
||||||
const quotationInclude = {
|
const quotationInclude = {
|
||||||
selectedTariff: true,
|
selectedTariff: true,
|
||||||
@@ -440,6 +441,18 @@ function teamAccessWhere(access: { teamUuid?: string }): { teamUuid?: string } {
|
|||||||
return access.teamUuid ? { teamUuid: access.teamUuid } : {}
|
return access.teamUuid ? { teamUuid: access.teamUuid } : {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ensureClientWorkspaceData(access: {
|
||||||
|
teamUuid?: string
|
||||||
|
userId: string
|
||||||
|
isManager: boolean
|
||||||
|
}): Promise<void> {
|
||||||
|
if (access.isManager || !access.teamUuid) return
|
||||||
|
await ensureDemoWorkspaceData({
|
||||||
|
teamUuid: access.teamUuid,
|
||||||
|
userId: access.userId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function mapTariffReference(item: PrismaTariffReference) {
|
function mapTariffReference(item: PrismaTariffReference) {
|
||||||
return {
|
return {
|
||||||
uuid: item.uuid,
|
uuid: item.uuid,
|
||||||
@@ -951,6 +964,7 @@ export const teamResolvers = {
|
|||||||
Query: {
|
Query: {
|
||||||
getTeamOrders: async (_: unknown, __: unknown, ctx: AuthContext) => {
|
getTeamOrders: async (_: unknown, __: unknown, ctx: AuthContext) => {
|
||||||
const access = assertTeamAccess(ctx)
|
const access = assertTeamAccess(ctx)
|
||||||
|
await ensureClientWorkspaceData(access)
|
||||||
|
|
||||||
const localOrders = await prisma.order.findMany({
|
const localOrders = await prisma.order.findMany({
|
||||||
where: {
|
where: {
|
||||||
@@ -981,6 +995,7 @@ export const teamResolvers = {
|
|||||||
|
|
||||||
quotations: async (_: unknown, args: { status?: string | null }, ctx: AuthContext) => {
|
quotations: async (_: unknown, args: { status?: string | null }, ctx: AuthContext) => {
|
||||||
const access = assertTeamAccess(ctx)
|
const access = assertTeamAccess(ctx)
|
||||||
|
await ensureClientWorkspaceData(access)
|
||||||
|
|
||||||
const items = await prisma.quotation.findMany({
|
const items = await prisma.quotation.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
Reference in New Issue
Block a user