fix production issue

This commit is contained in:
root
2026-05-10 19:55:57 -04:00
parent a224d7704a
commit 2d28947b92
17 changed files with 107 additions and 81 deletions
@@ -45,36 +45,37 @@ export function HomepageLayoutEditor({ layout, availableSections, onChange, onAd
useEffect(() => {
if (!interaction) return
const snap = interaction
function handlePointerMove(event: PointerEvent) {
const canvas = canvasRef.current
if (!canvas) return
const rect = canvas.getBoundingClientRect()
const colWidth = rect.width / PUBLIC_HOMEPAGE_GRID_COLUMNS
const deltaCols = Math.round((event.clientX - interaction.startX) / colWidth)
const deltaRows = Math.round((event.clientY - interaction.startY) / ROW_HEIGHT)
const limits = getPublicHomepageSectionLimits(interaction.origin.type)
const deltaCols = Math.round((event.clientX - snap.startX) / colWidth)
const deltaRows = Math.round((event.clientY - snap.startY) / ROW_HEIGHT)
const limits = getPublicHomepageSectionLimits(snap.origin.type)
onChange({
items: layout.items.map((item) => {
if (item.id !== interaction.itemId) return item
if (interaction.mode === 'move') {
if (item.id !== snap.itemId) return item
if (snap.mode === 'move') {
return {
...item,
x: clamp(interaction.origin.x + deltaCols, 1, PUBLIC_HOMEPAGE_GRID_COLUMNS - interaction.origin.w + 1),
y: clamp(interaction.origin.y + deltaRows, 1, PUBLIC_HOMEPAGE_GRID_ROWS - interaction.origin.h + 1),
x: clamp(snap.origin.x + deltaCols, 1, PUBLIC_HOMEPAGE_GRID_COLUMNS - snap.origin.w + 1),
y: clamp(snap.origin.y + deltaRows, 1, PUBLIC_HOMEPAGE_GRID_ROWS - snap.origin.h + 1),
}
}
const nextW = clamp(
interaction.origin.w + deltaCols,
snap.origin.w + deltaCols,
limits.minW,
Math.min(limits.maxW, PUBLIC_HOMEPAGE_GRID_COLUMNS - interaction.origin.x + 1),
Math.min(limits.maxW, PUBLIC_HOMEPAGE_GRID_COLUMNS - snap.origin.x + 1),
)
const nextH = clamp(
interaction.origin.h + deltaRows,
snap.origin.h + deltaRows,
limits.minH,
Math.min(limits.maxH, PUBLIC_HOMEPAGE_GRID_ROWS - interaction.origin.y + 1),
Math.min(limits.maxH, PUBLIC_HOMEPAGE_GRID_ROWS - snap.origin.y + 1),
)
return {
+6 -6
View File
@@ -487,7 +487,7 @@ router.patch('/companies/:id', requireAdminAuth, requireAdminRole('SUPPORT'), as
currentPeriodEnd: parseOptionalDate(body.subscription.currentPeriodEnd),
cancelledAt: parseOptionalDate(body.subscription.cancelledAt),
cancelAtPeriodEnd: body.subscription.cancelAtPeriodEnd ?? false,
},
} as any,
})
}
@@ -502,7 +502,7 @@ router.patch('/companies/:id', requireAdminAuth, requireAdminRole('SUPPORT'), as
subdomain: body.brand.subdomain ?? currentCompany.slug,
paymentMethodsEnabled: before.brand?.paymentMethodsEnabled ?? [],
...body.brand,
},
} as any,
})
}
@@ -513,7 +513,7 @@ router.patch('/companies/:id', requireAdminAuth, requireAdminRole('SUPPORT'), as
create: {
companyId: req.params.id,
...body.contractSettings,
},
} as any,
})
}
@@ -524,7 +524,7 @@ router.patch('/companies/:id', requireAdminAuth, requireAdminRole('SUPPORT'), as
create: {
companyId: req.params.id,
...body.accountingSettings,
},
} as any,
})
}
@@ -721,7 +721,7 @@ router.patch('/admins/:id/permissions', requireAdminAuth, requireAdminRole('SUPE
adminUserId: req.params.id,
resource: permission.resource,
actions: permission.actions,
})),
})) as any,
}),
])
const admin = await prisma.adminUser.findUniqueOrThrow({
@@ -762,7 +762,7 @@ router.get('/billing', requireAdminAuth, requireAdminRole('FINANCE'), async (req
prisma.subscription.count({ where }),
])
const activeStatuses = ['ACTIVE', 'TRIALING']
const activeStatuses = ['ACTIVE', 'TRIALING'] as any[]
const allActive = await prisma.subscription.findMany({
where: { status: { in: activeStatuses } },
select: { plan: true, billingPeriod: true, currency: true },
+1 -1
View File
@@ -217,7 +217,7 @@ router.patch('/me', async (req, res, next) => {
const body = companySchema.parse(req.body)
const company = await prisma.company.update({
where: { id: req.companyId },
data: body,
data: body as any,
})
res.json({ data: company })
} catch (err) { next(err) }
+2 -2
View File
@@ -59,7 +59,7 @@ router.post('/', async (req, res, next) => {
dateOfBirth: body.dateOfBirth ? new Date(body.dateOfBirth) : null,
licenseExpiry: body.licenseExpiry ? new Date(body.licenseExpiry) : null,
licenseIssuedAt: body.licenseIssuedAt ? new Date(body.licenseIssuedAt) : null,
},
} as any,
})
if (body.licenseExpiry) {
await validateAndFlagLicense(customer.id).catch(() => null)
@@ -81,7 +81,7 @@ router.get('/:id', async (req, res, next) => {
router.patch('/:id', async (req, res, next) => {
try {
const body = customerSchema.partial().parse(req.body)
const updated = await prisma.customer.updateMany({ where: { id: req.params.id, companyId: req.companyId }, data: { ...body, dateOfBirth: body.dateOfBirth ? new Date(body.dateOfBirth) : undefined, licenseExpiry: body.licenseExpiry ? new Date(body.licenseExpiry) : undefined, licenseIssuedAt: body.licenseIssuedAt ? new Date(body.licenseIssuedAt) : undefined } })
const updated = await prisma.customer.updateMany({ where: { id: req.params.id, companyId: req.companyId }, data: { ...body, dateOfBirth: body.dateOfBirth ? new Date(body.dateOfBirth) : undefined, licenseExpiry: body.licenseExpiry ? new Date(body.licenseExpiry) : undefined, licenseIssuedAt: body.licenseIssuedAt ? new Date(body.licenseIssuedAt) : undefined } as any })
if (updated.count === 0) return res.status(404).json({ error: 'not_found', message: 'Customer not found', statusCode: 404 })
if (body.licenseExpiry) await validateAndFlagLicense(req.params.id).catch(() => null)
const customer = await prisma.customer.findUniqueOrThrow({ where: { id: req.params.id } })
+1 -1
View File
@@ -190,7 +190,7 @@ router.post('/reservations/:id/capture-paypal', async (req, res, next) => {
where: { paypalCaptureId: paypalOrderId, companyId: req.companyId },
})
const capture = await paypal.captureOrder(paypalOrderId)
const capture = await paypal.captureOrder(paypalOrderId) as Record<string, any>
const captureId = capture.purchase_units?.[0]?.payments?.captures?.[0]?.id ?? paypalOrderId
const updated = await prisma.rentalPayment.update({
+1 -1
View File
@@ -156,7 +156,7 @@ router.post('/', async (req, res, next) => {
}
const baseAmount = vehicle.dailyRate * totalDays
const { applied, total: pricingTotal } = await applyPricingRules(req.companyId, body.customerId, body.additionalDrivers, vehicle.dailyRate, totalDays)
const { applied, total: pricingTotal } = await applyPricingRules(req.companyId, body.customerId, body.additionalDrivers as any[], vehicle.dailyRate, totalDays)
const totalAmount = baseAmount - discountAmount + pricingTotal + body.depositAmount
const reservation = await prisma.reservation.create({
+2 -2
View File
@@ -291,7 +291,7 @@ router.post('/:slug/book', async (req, res, next) => {
const { applied, total: pricingRulesTotal } = await applyPricingRules(
company.id,
customer.id,
body.additionalDrivers,
body.additionalDrivers as any[],
vehicle.dailyRate,
totalDays,
)
@@ -484,7 +484,7 @@ router.post('/:slug/booking/:id/capture-paypal', async (req, res, next) => {
where: { paypalCaptureId: paypalOrderId, companyId: company.id },
})
const capture = await paypal.captureOrder(paypalOrderId)
const capture = await paypal.captureOrder(paypalOrderId) as Record<string, any>
const captureId = capture.purchase_units?.[0]?.payments?.captures?.[0]?.id ?? paypalOrderId
await prisma.rentalPayment.update({
+1 -1
View File
@@ -101,7 +101,7 @@ router.post('/capture-paypal', requireCompanyAuth, requireTenant, async (req, re
include: { subscription: true },
})
const capture = await paypal.captureOrder(paypalOrderId)
const capture = await paypal.captureOrder(paypalOrderId) as Record<string, any>
const captureId = capture.purchase_units?.[0]?.payments?.captures?.[0]?.id ?? paypalOrderId
await prisma.subscriptionInvoice.update({
@@ -1,5 +1,6 @@
import { AdditionalDriverCharge } from '@rentaldrivego/database'
import { prisma } from '../lib/prisma'
type AdditionalDriverCharge = 'PER_DAY' | 'FLAT' | 'FREE'
import { validateLicense } from './licenseValidationService'
export interface AdditionalDriverInput {
+8 -8
View File
@@ -50,15 +50,15 @@ export async function createCheckout(params: AmanPayCreateParams): Promise<AmanP
})
if (!res.ok) {
const err = await res.json().catch(() => ({}))
throw new Error(`AmanPay checkout failed: ${err?.message ?? res.statusText}`)
const err = await res.json().catch(() => ({})) as Record<string, unknown>
throw new Error(`AmanPay checkout failed: ${(err?.message as string) ?? res.statusText}`)
}
const json = await res.json()
const json = await res.json() as Record<string, unknown>
return {
transactionId: json.transaction_id ?? json.id,
checkoutUrl: json.checkout_url ?? json.payment_url,
status: json.status ?? 'PENDING',
transactionId: (json.transaction_id ?? json.id) as string,
checkoutUrl: (json.checkout_url ?? json.payment_url) as string,
status: (json.status ?? 'PENDING') as string,
}
}
@@ -77,8 +77,8 @@ export async function refundTransaction(transactionId: string, amount: number, r
body: JSON.stringify({ amount, reason }),
})
if (!res.ok) {
const err = await res.json().catch(() => ({}))
throw new Error(`AmanPay refund failed: ${err?.message ?? res.statusText}`)
const err = await res.json().catch(() => ({})) as Record<string, unknown>
throw new Error(`AmanPay refund failed: ${(err?.message as string) ?? res.statusText}`)
}
return res.json()
}
+24 -22
View File
@@ -2,7 +2,9 @@ import { execFile } from 'child_process'
import { promisify } from 'util'
import * as fs from 'fs/promises'
import * as path from 'path'
import { prisma } from '../lib/prisma'
import { prisma as _prisma } from '../lib/prisma'
const db = _prisma as any
const execFileAsync = promisify(execFile)
let composeCommand: 'docker-compose' | 'docker' | null = null
@@ -72,12 +74,12 @@ async function composeFilesExist(): Promise<boolean> {
}
async function rebuildServicesFromDatabase(): Promise<ServicesMap> {
const records = await prisma.companyContainer.findMany({
const records = await db.companyContainer.findMany({
include: { company: { select: { slug: true } } },
})
return Object.fromEntries(
records.map((record) => [
records.map((record: any) => [
serviceName(record.company.slug),
{
companyId: record.companyId,
@@ -225,7 +227,7 @@ function mapDockerError(err: unknown) {
}
async function setContainerError(companyId: string, message: string): Promise<void> {
await prisma.companyContainer.update({
await db.companyContainer.update({
where: { companyId },
data: { status: 'ERROR', errorMessage: message },
}).catch(() => null)
@@ -239,7 +241,7 @@ async function runContainerAction(
preStatus?: Exclude<ServiceStatus, 'ERROR'>,
): Promise<void> {
if (preStatus) {
await prisma.companyContainer.update({ where: { companyId }, data: { status: preStatus, errorMessage: null } })
await db.companyContainer.update({ where: { companyId }, data: { status: preStatus, errorMessage: null } })
}
try {
@@ -248,7 +250,7 @@ async function runContainerAction(
? ['up', '-d', '--no-recreate', serviceName(slug)]
: [command, serviceName(slug)]
await compose(...args)
await prisma.companyContainer.update({
await db.companyContainer.update({
where: { companyId },
data: { status: successStatus, errorMessage: null },
})
@@ -260,7 +262,7 @@ async function runContainerAction(
}
async function allocatePort(): Promise<number> {
const last = await prisma.companyContainer.findFirst({
const last = await db.companyContainer.findFirst({
orderBy: { port: 'desc' },
select: { port: true },
})
@@ -304,7 +306,7 @@ export async function createCompanyContainer(company: {
const name = serviceName(company.slug)
const port = await allocatePort()
const record = await prisma.companyContainer.create({
const record = await db.companyContainer.create({
data: {
companyId: company.id,
containerName: containerName(company.slug),
@@ -322,12 +324,12 @@ export async function createCompanyContainer(company: {
await compose('up', '-d', '--no-recreate', name)
const dockerId = await getDockerServiceId(company.slug)
await prisma.companyContainer.update({
await db.companyContainer.update({
where: { id: record.id },
data: { dockerId, status: 'RUNNING' },
})
} catch (err) {
await prisma.companyContainer.update({
await db.companyContainer.update({
where: { id: record.id },
data: {
status: 'ERROR',
@@ -339,7 +341,7 @@ export async function createCompanyContainer(company: {
}
export async function startCompanyContainer(companyId: string): Promise<void> {
const record = await prisma.companyContainer.findUniqueOrThrow({
const record = await db.companyContainer.findUniqueOrThrow({
where: { companyId },
include: { company: { select: { slug: true } } },
})
@@ -348,7 +350,7 @@ export async function startCompanyContainer(companyId: string): Promise<void> {
}
export async function stopCompanyContainer(companyId: string): Promise<void> {
const record = await prisma.companyContainer.findUniqueOrThrow({
const record = await db.companyContainer.findUniqueOrThrow({
where: { companyId },
include: { company: { select: { slug: true } } },
})
@@ -357,7 +359,7 @@ export async function stopCompanyContainer(companyId: string): Promise<void> {
}
export async function restartCompanyContainer(companyId: string): Promise<void> {
const record = await prisma.companyContainer.findUniqueOrThrow({
const record = await db.companyContainer.findUniqueOrThrow({
where: { companyId },
include: { company: { select: { slug: true } } },
})
@@ -366,13 +368,13 @@ export async function restartCompanyContainer(companyId: string): Promise<void>
}
export async function removeCompanyContainer(companyId: string): Promise<void> {
const record = await prisma.companyContainer.findUniqueOrThrow({
const record = await db.companyContainer.findUniqueOrThrow({
where: { companyId },
include: { company: { select: { slug: true } } },
})
const name = serviceName(record.company.slug)
await prisma.companyContainer.update({ where: { companyId }, data: { status: 'REMOVING' } })
await db.companyContainer.update({ where: { companyId }, data: { status: 'REMOVING' } })
try {
await compose('stop', name)
@@ -386,7 +388,7 @@ export async function removeCompanyContainer(companyId: string): Promise<void> {
delete services[name]
await writeServices(services)
await prisma.companyContainer.delete({ where: { companyId } })
await db.companyContainer.delete({ where: { companyId } })
}
export async function redeployCompanyContainer(company: {
@@ -396,11 +398,11 @@ export async function redeployCompanyContainer(company: {
}): Promise<void> {
const name = serviceName(company.slug)
const existing = await prisma.companyContainer.findUnique({ where: { companyId: company.id } })
const existing = await db.companyContainer.findUnique({ where: { companyId: company.id } })
if (existing) {
try { await compose('stop', name) } catch { /* ok */ }
try { await compose('rm', '-f', name) } catch { /* ok */ }
await prisma.companyContainer.delete({ where: { companyId: company.id } })
await db.companyContainer.delete({ where: { companyId: company.id } })
}
// Remove from compose file too, then recreate
@@ -412,7 +414,7 @@ export async function redeployCompanyContainer(company: {
}
export async function getContainerLogs(companyId: string, tail = 150): Promise<string> {
const record = await prisma.companyContainer.findUniqueOrThrow({
const record = await db.companyContainer.findUniqueOrThrow({
where: { companyId },
include: { company: { select: { slug: true } } },
})
@@ -431,7 +433,7 @@ export async function getContainerLogs(companyId: string, tail = 150): Promise<s
}
export async function syncContainerStatuses(): Promise<void> {
const records = await prisma.companyContainer.findMany({
const records = await db.companyContainer.findMany({
where: { status: { notIn: ['PENDING', 'CREATING', 'REMOVING'] } },
include: { company: { select: { slug: true } } },
})
@@ -451,13 +453,13 @@ export async function syncContainerStatuses(): Promise<void> {
const stateByService = Object.fromEntries(rows.map((r) => [r.Service, r.State]))
await Promise.allSettled(
records.map(async (rec) => {
records.map(async (rec: any) => {
const name = serviceName(rec.company.slug)
const dockerState = stateByService[name]
const status = dockerState ? mapDockerState(dockerState) : 'STOPPED'
if (rec.status !== status) {
await prisma.companyContainer.update({ where: { id: rec.id }, data: { status } })
await db.companyContainer.update({ where: { id: rec.id }, data: { status } })
}
}),
)
+2 -2
View File
@@ -135,7 +135,7 @@ export async function sendNotification(opts: SendNotificationOptions) {
type: opts.type,
title: opts.title,
body: opts.body,
data: opts.data ?? {},
data: (opts.data ?? {}) as any,
channel,
status: 'PENDING',
companyId: opts.companyId ?? null,
@@ -181,7 +181,7 @@ export async function sendNotification(opts: SendNotificationOptions) {
: process.env.MAIL_REPLY_TO_ADDRESS
: undefined,
})
providerMessageId = info.messageId
providerMessageId = info.messageId ?? null
success = true
} else {
throw new Error('No email provider is configured')
+9 -9
View File
@@ -19,8 +19,8 @@ async function getAccessToken(): Promise<string> {
})
if (!res.ok) throw new Error('PayPal: failed to obtain access token')
const json = await res.json()
cachedToken = { token: json.access_token, expiresAt: Date.now() + json.expires_in * 1000 }
const json = await res.json() as Record<string, unknown>
cachedToken = { token: json.access_token as string, expiresAt: Date.now() + (json.expires_in as number) * 1000 }
return cachedToken.token
}
@@ -35,10 +35,10 @@ async function ppFetch(path: string, init?: RequestInit) {
},
})
if (!res.ok) {
const err = await res.json().catch(() => ({}))
throw new Error(`PayPal ${path} failed: ${err?.message ?? res.statusText}`)
const err = await res.json().catch(() => ({})) as Record<string, unknown>
throw new Error(`PayPal ${path} failed: ${(err?.message as string) ?? res.statusText}`)
}
return res.json()
return res.json() as Promise<Record<string, unknown>>
}
export interface PayPalCreateOrderParams {
@@ -80,11 +80,11 @@ export async function createOrder(params: PayPalCreateOrderParams): Promise<PayP
}),
})
const approveLink = json.links?.find((l: { rel: string; href: string }) => l.rel === 'approve')
const approveLink = (json.links as Array<{ rel: string; href: string }> | undefined)?.find((l) => l.rel === 'approve')
return {
orderId: json.id,
orderId: json.id as string,
approveUrl: approveLink?.href ?? '',
status: json.status,
status: json.status as string,
}
}
@@ -116,7 +116,7 @@ export async function verifyWebhookEvent(headers: Record<string, string | undefi
webhook_event: JSON.parse(rawBody),
}),
})
return json.verification_status === 'SUCCESS'
return (json as Record<string, unknown>).verification_status === 'SUCCESS'
} catch {
return false
}
@@ -1,7 +1,7 @@
'use client'
import { useEffect, useMemo, useState } from 'react'
import { useTeam, TeamMember } from '@/hooks/useTeam'
import { useTeam, TeamMember, InvitePayload } from '@/hooks/useTeam'
import InviteModal from '@/components/team/InviteModal'
import EditMemberModal from '@/components/team/EditMemberModal'
import PermissionsMatrix from '@/components/team/PermissionsMatrix'
@@ -122,7 +122,7 @@ export default function TeamPage() {
})
}, [members, search, roleFilter, statusFilter])
const handleInvite: typeof invite = async (payload) => {
const handleInvite = async (payload: InvitePayload) => {
await invite(payload)
showToast(`Invite sent to ${payload.email}`)
}
@@ -39,8 +39,8 @@ export default function Sidebar() {
const pathname = usePathname()
const router = useRouter()
const isActive = (item: typeof NAV_ITEMS[0]) => {
if (item.exact) return pathname === item.href
const isActive = (item: typeof NAV_ITEMS[number]) => {
if ('exact' in item && item.exact) return pathname === item.href
return pathname.startsWith(item.href)
}
+22
View File
@@ -22,3 +22,25 @@ Your data stays as long as you do not run docker compose ... down -v or manually
If you later change the schema and want to apply it without wiping data, run:
docker compose -f docker-compose.dev.yml run --rm migrate sh -c "npm run db:deploy"
Production was already using a persistent named Postgres volume, so the main issue was the dev bootstrap behavior. Restart the dev stack once for this change to take effect.
Done. The DOCKER.md production section now covers the full deployment flow from a fresh server. The short version:
# Once per server
docker network create traefik-proxy
# Clone & configure
git clone <repo> rentaldrivego && cd rentaldrivego
cp .env.docker.production.example .env.docker.production
# → fill in POSTGRES_PASSWORD, JWT_SECRET, ACME_EMAIL, RESEND_API_KEY
# Deploy
docker compose -f traefik.yaml up -d
docker compose -f docker-compose.production.yml up --build -d
+9 -9
View File
@@ -100,12 +100,12 @@ export const defaultMarketplaceHomepageContent: MarketplaceHomepageConfig = {
['Unified publishing', 'Vehicle photos, pricing, and offers flow from one admin workflow into marketplace discovery and branded booking pages.'],
['Qualified discovery', 'Featured offers, location-aware browsing, and company reputation signals help renters narrow options fast.'],
['Direct revenue path', 'Bookings move into the companys own payment flow, so customer ownership and cash collection stay with the business.'],
].map(([title, body]) => ({ title, body })),
].map(([title, body]) => ({ title: title!, body: body! })),
metrics: [
['01', 'Shared marketplace visibility'],
['02', 'Direct company checkout'],
['03', 'Company-owned payments'],
].map(([value, label]) => ({ value, label })),
].map(([value, label]) => ({ value: value!, label: label! })),
featureLabel: 'What the product covers',
features: [
'Fleet management with multi-photo uploads',
@@ -118,7 +118,7 @@ export const defaultMarketplaceHomepageContent: MarketplaceHomepageConfig = {
['1', 'Create your company workspace', 'Pick a plan, launch your 14-day trial, and verify the owner account.'],
['2', 'Publish vehicles and offers', 'Upload photos once and control what appears on the marketplace and branded site.'],
['3', 'Accept bookings on your own site', 'Renters discover your fleet on RentalDriveGo, then pay you directly on your branded booking flow.'],
].map(([step, title, body]) => ({ step, title, body })),
].map(([step, title, body]) => ({ step: step!, title: title!, body: body! })),
stepLabel: 'Step',
readyKicker: 'Ready to launch',
readyTitle: 'A marketplace homepage should sell the system in seconds.',
@@ -155,12 +155,12 @@ export const defaultMarketplaceHomepageContent: MarketplaceHomepageConfig = {
['Publication unifiée', 'Les photos, tarifs et offres circulent depuis un seul flux admin vers la découverte marketplace et les pages de réservation de marque.'],
['Découverte qualifiée', 'Offres mises en avant, navigation par localisation et signaux de réputation aident les clients à filtrer rapidement.'],
['Revenus en direct', 'Les réservations basculent vers le paiement propre à lentreprise, pour garder la relation client et lencaissement.'],
].map(([title, body]) => ({ title, body })),
].map(([title, body]) => ({ title: title!, body: body! })),
metrics: [
['01', 'Visibilité marketplace partagée'],
['02', 'Paiement direct entreprise'],
['03', 'Paiements détenus par la société'],
].map(([value, label]) => ({ value, label })),
].map(([value, label]) => ({ value: value!, label: label! })),
featureLabel: 'Ce que couvre le produit',
features: [
'Gestion de flotte avec téléversement multi-photos',
@@ -173,7 +173,7 @@ export const defaultMarketplaceHomepageContent: MarketplaceHomepageConfig = {
['1', 'Créez votre espace entreprise', 'Choisissez un plan, lancez votre essai de 14 jours et vérifiez le compte propriétaire.'],
['2', 'Publiez véhicules et offres', 'Téléversez une seule fois et contrôlez ce qui apparaît sur la marketplace et le site de marque.'],
['3', 'Acceptez les réservations sur votre site', 'Les clients découvrent votre flotte sur RentalDriveGo puis paient directement via votre parcours de réservation.'],
].map(([step, title, body]) => ({ step, title, body })),
].map(([step, title, body]) => ({ step: step!, title: title!, body: body! })),
stepLabel: 'Étape',
readyKicker: 'Prêt à démarrer',
readyTitle: 'Une homepage marketplace doit vendre le système en quelques secondes.',
@@ -210,12 +210,12 @@ export const defaultMarketplaceHomepageContent: MarketplaceHomepageConfig = {
['نشر موحد', 'صور السيارات والأسعار والعروض تنتقل من تدفق إدارة واحد إلى السوق وصفحات الحجز ذات الهوية الخاصة.'],
['اكتشاف مؤهل', 'العروض المميزة والتصفح حسب الموقع وإشارات السمعة تساعد المستأجرين على تضييق الخيارات بسرعة.'],
['مسار إيراد مباشر', 'تنتقل الحجوزات إلى صفحة الدفع الخاصة بالشركة حتى تبقى الملكية المالية وعلاقة العميل مع النشاط نفسه.'],
].map(([title, body]) => ({ title, body })),
].map(([title, body]) => ({ title: title!, body: body! })),
metrics: [
['01', 'ظهور مشترك في السوق'],
['02', 'دفع مباشر للشركة'],
['03', 'مدفوعات مملوكة للشركة'],
].map(([value, label]) => ({ value, label })),
].map(([value, label]) => ({ value: value!, label: label! })),
featureLabel: 'ما الذي يغطيه المنتج',
features: [
'إدارة الأسطول مع رفع عدة صور',
@@ -228,7 +228,7 @@ export const defaultMarketplaceHomepageContent: MarketplaceHomepageConfig = {
['1', 'أنشئ مساحة شركتك', 'اختر الخطة وابدأ تجربتك لمدة 14 يوماً ثم تحقق من حساب المالك.'],
['2', 'انشر السيارات والعروض', 'ارفع الصور مرة واحدة وتحكم فيما يظهر في السوق وفي الموقع المخصص.'],
['3', 'استقبل الحجوزات على موقعك', 'يكتشف المستأجرون أسطولك على RentalDriveGo ثم يدفعون مباشرة عبر مسار الحجز الخاص بك.'],
].map(([step, title, body]) => ({ step, title, body })),
].map(([step, title, body]) => ({ step: step!, title: title!, body: body! })),
stepLabel: 'الخطوة',
readyKicker: 'جاهز للانطلاق',
readyTitle: 'يجب أن تشرح الصفحة الرئيسية قيمة المنصة خلال ثوانٍ.',