diff --git a/apps/admin/src/app/dashboard/companies/[id]/HomepageLayoutEditor.tsx b/apps/admin/src/app/dashboard/companies/[id]/HomepageLayoutEditor.tsx index 30a38bc..ec37300 100644 --- a/apps/admin/src/app/dashboard/companies/[id]/HomepageLayoutEditor.tsx +++ b/apps/admin/src/app/dashboard/companies/[id]/HomepageLayoutEditor.tsx @@ -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 { diff --git a/apps/api/src/routes/admin.ts b/apps/api/src/routes/admin.ts index 358443d..df946d9 100644 --- a/apps/api/src/routes/admin.ts +++ b/apps/api/src/routes/admin.ts @@ -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 }, diff --git a/apps/api/src/routes/companies.ts b/apps/api/src/routes/companies.ts index 3a1129d..cd24da0 100644 --- a/apps/api/src/routes/companies.ts +++ b/apps/api/src/routes/companies.ts @@ -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) } diff --git a/apps/api/src/routes/customers.ts b/apps/api/src/routes/customers.ts index 834e510..17d1851 100644 --- a/apps/api/src/routes/customers.ts +++ b/apps/api/src/routes/customers.ts @@ -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 } }) diff --git a/apps/api/src/routes/payments.ts b/apps/api/src/routes/payments.ts index dc62be5..57316a1 100644 --- a/apps/api/src/routes/payments.ts +++ b/apps/api/src/routes/payments.ts @@ -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 const captureId = capture.purchase_units?.[0]?.payments?.captures?.[0]?.id ?? paypalOrderId const updated = await prisma.rentalPayment.update({ diff --git a/apps/api/src/routes/reservations.ts b/apps/api/src/routes/reservations.ts index 7c90920..0f0b30f 100644 --- a/apps/api/src/routes/reservations.ts +++ b/apps/api/src/routes/reservations.ts @@ -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({ diff --git a/apps/api/src/routes/site.ts b/apps/api/src/routes/site.ts index 68c5900..a2c5c70 100644 --- a/apps/api/src/routes/site.ts +++ b/apps/api/src/routes/site.ts @@ -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 const captureId = capture.purchase_units?.[0]?.payments?.captures?.[0]?.id ?? paypalOrderId await prisma.rentalPayment.update({ diff --git a/apps/api/src/routes/subscriptions.ts b/apps/api/src/routes/subscriptions.ts index 5a52c2f..3198c0f 100644 --- a/apps/api/src/routes/subscriptions.ts +++ b/apps/api/src/routes/subscriptions.ts @@ -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 const captureId = capture.purchase_units?.[0]?.payments?.captures?.[0]?.id ?? paypalOrderId await prisma.subscriptionInvoice.update({ diff --git a/apps/api/src/services/additionalDriverService.ts b/apps/api/src/services/additionalDriverService.ts index 7e9b5a3..8b03c57 100644 --- a/apps/api/src/services/additionalDriverService.ts +++ b/apps/api/src/services/additionalDriverService.ts @@ -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 { diff --git a/apps/api/src/services/amanpayService.ts b/apps/api/src/services/amanpayService.ts index 636bc50..40e3354 100644 --- a/apps/api/src/services/amanpayService.ts +++ b/apps/api/src/services/amanpayService.ts @@ -50,15 +50,15 @@ export async function createCheckout(params: AmanPayCreateParams): Promise ({})) - throw new Error(`AmanPay checkout failed: ${err?.message ?? res.statusText}`) + const err = await res.json().catch(() => ({})) as Record + throw new Error(`AmanPay checkout failed: ${(err?.message as string) ?? res.statusText}`) } - const json = await res.json() + const json = await res.json() as Record 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 + throw new Error(`AmanPay refund failed: ${(err?.message as string) ?? res.statusText}`) } return res.json() } diff --git a/apps/api/src/services/containerService.ts b/apps/api/src/services/containerService.ts index 56a446c..d3c9df6 100644 --- a/apps/api/src/services/containerService.ts +++ b/apps/api/src/services/containerService.ts @@ -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 { } async function rebuildServicesFromDatabase(): Promise { - 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 { - 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, ): Promise { 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 { - 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 { - 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 { } export async function stopCompanyContainer(companyId: string): Promise { - 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 { } export async function restartCompanyContainer(companyId: string): Promise { - 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 } export async function removeCompanyContainer(companyId: string): Promise { - 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 { 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 { 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 { - 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 { - 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 { 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 } }) } }), ) diff --git a/apps/api/src/services/notificationService.ts b/apps/api/src/services/notificationService.ts index 70a98f3..6eb3987 100644 --- a/apps/api/src/services/notificationService.ts +++ b/apps/api/src/services/notificationService.ts @@ -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') diff --git a/apps/api/src/services/paypalService.ts b/apps/api/src/services/paypalService.ts index 7eb68af..938ddcb 100644 --- a/apps/api/src/services/paypalService.ts +++ b/apps/api/src/services/paypalService.ts @@ -19,8 +19,8 @@ async function getAccessToken(): Promise { }) 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 + 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 + throw new Error(`PayPal ${path} failed: ${(err?.message as string) ?? res.statusText}`) } - return res.json() + return res.json() as Promise> } export interface PayPalCreateOrderParams { @@ -80,11 +80,11 @@ export async function createOrder(params: PayPalCreateOrderParams): Promise 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).verification_status === 'SUCCESS' } catch { return false } diff --git a/apps/dashboard/src/app/(dashboard)/dashboard/team/page.tsx b/apps/dashboard/src/app/(dashboard)/dashboard/team/page.tsx index b0f668a..75989e9 100644 --- a/apps/dashboard/src/app/(dashboard)/dashboard/team/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/dashboard/team/page.tsx @@ -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}`) } diff --git a/apps/dashboard/src/components/layout/Sidebar.tsx b/apps/dashboard/src/components/layout/Sidebar.tsx index 746c8b1..51bb0b2 100644 --- a/apps/dashboard/src/components/layout/Sidebar.tsx +++ b/apps/dashboard/src/components/layout/Sidebar.tsx @@ -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) } diff --git a/command_to_run_dev.txt b/command_to_run_dev.txt index 6c2b528..ab90b05 100644 --- a/command_to_run_dev.txt +++ b/command_to_run_dev.txt @@ -21,4 +21,26 @@ docker compose -f docker-compose.dev.yml --profile full up --build will no longe Your data stays as long as you do not run docker compose ... down -v or manually delete the Docker volumes. 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. \ No newline at end of file +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 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 \ No newline at end of file diff --git a/packages/types/src/marketplace-homepage.ts b/packages/types/src/marketplace-homepage.ts index c5bb86d..48bb018 100644 --- a/packages/types/src/marketplace-homepage.ts +++ b/packages/types/src/marketplace-homepage.ts @@ -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 company’s 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 à l’entreprise, pour garder la relation client et l’encaissement.'], - ].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: 'يجب أن تشرح الصفحة الرئيسية قيمة المنصة خلال ثوانٍ.',