chore: wire Carplace into dev and production stacks
Build & Deploy / Build & Push Docker Image (push) Successful in 2m55s
Test / Type Check (all packages) (push) Successful in 54s
Build & Deploy / Deploy to VPS (push) Successful in 4s
Test / API Unit Tests (push) Failing after 48s
Test / Homepage Unit Tests (push) Successful in 43s
Test / Storefront Unit Tests (push) Failing after 40s
Test / Admin Unit Tests (push) Successful in 40s
Test / Dashboard Unit Tests (push) Failing after 42s
Test / API Integration Tests (push) Successful in 1m0s

This commit is contained in:
root
2026-07-02 18:15:42 -04:00
parent ad5f5ebab7
commit 7ff2dbb139
214 changed files with 5258 additions and 5906 deletions
@@ -113,7 +113,7 @@ function offerToForm(o: Offer): FormState {
const copy = {
en: {
title: 'Offers',
subtitle: 'Promotions shown on your site and optionally on the storefront.',
subtitle: 'Promotions shown on your site and optionally on Carplace.',
createOffer: 'Create Offer',
editOffer: 'Edit Offer',
deleteOffer: 'Delete Offer',
@@ -126,7 +126,7 @@ const copy = {
ends: 'ends',
active: 'Active',
inactive: 'Inactive',
storefront: 'Storefront',
carplace: 'Carplace',
featured: 'Featured',
empty: 'No offers yet. Create your first offer to attract customers.',
labelTitle: 'Title *',
@@ -146,7 +146,7 @@ const copy = {
labelValidFrom: 'Valid From *',
labelValidUntil: 'Valid Until *',
labelIsActive: 'Active',
labelIsPublic: 'Show on storefront',
labelIsPublic: 'Show on Carplace',
labelIsFeatured: 'Featured',
typePERCENTAGE: 'Percentage (%)',
typeFIXED_AMOUNT: 'Fixed Amount (MAD)',
@@ -174,7 +174,7 @@ const copy = {
},
fr: {
title: 'Offres',
subtitle: 'Promotions affichées sur votre site et éventuellement sur la storefront.',
subtitle: 'Promotions affichées sur votre site et éventuellement sur Carplace.',
createOffer: 'Créer une offre',
editOffer: "Modifier l'offre",
deleteOffer: "Supprimer l'offre",
@@ -187,7 +187,7 @@ const copy = {
ends: 'expire le',
active: 'Actif',
inactive: 'Inactif',
storefront: 'Storefront',
carplace: 'Carplace',
featured: 'Mise en avant',
empty: 'Aucune offre. Créez votre première offre pour attirer des clients.',
labelTitle: 'Titre *',
@@ -207,7 +207,7 @@ const copy = {
labelValidFrom: 'Valable du *',
labelValidUntil: "Valable jusqu'au *",
labelIsActive: 'Actif',
labelIsPublic: 'Afficher sur la storefront',
labelIsPublic: 'Afficher sur Carplace',
labelIsFeatured: 'Mise en avant',
typePERCENTAGE: 'Pourcentage (%)',
typeFIXED_AMOUNT: 'Montant fixe (MAD)',
@@ -248,7 +248,7 @@ const copy = {
ends: 'تنتهي في',
active: 'نشط',
inactive: 'غير نشط',
storefront: 'السوق',
carplace: 'Carplace',
featured: 'مميز',
empty: 'لا توجد عروض بعد. أنشئ أول عرض لجذب العملاء.',
labelTitle: 'العنوان *',
@@ -514,7 +514,7 @@ export default function OffersPage() {
)}
<div className="flex flex-wrap gap-2">
{offer.isPublic && <span className="badge-blue">{t.storefront}</span>}
{offer.isPublic && <span className="badge-blue">{t.carplace}</span>}
{offer.isFeatured && <span className="badge-purple">{t.featured}</span>}
{offer.promoCode && <span className="badge-amber">{offer.promoCode}</span>}
{typeof offer.redemptionCount === 'number' && (
@@ -109,7 +109,7 @@ export default function OnlineReservationsPage() {
const copy = {
en: {
title: 'Online Reservations',
subtitle: 'Requests submitted by customers through the storefront. Confirm or decline each one.',
subtitle: 'Requests submitted by customers through Carplace. Confirm or decline each one.',
refresh: 'Refresh',
pendingApproval: 'Pending approval',
loading: 'Loading…',
@@ -122,7 +122,7 @@ export default function OnlineReservationsPage() {
},
fr: {
title: 'Réservations en ligne',
subtitle: 'Demandes envoyées par les clients via la storefront. Confirmez ou refusez chaque demande.',
subtitle: 'Demandes envoyées par les clients via Carplace. Confirmez ou refusez chaque demande.',
refresh: 'Actualiser',
pendingApproval: 'En attente de validation',
loading: 'Chargement…',
@@ -157,7 +157,7 @@ export default function OnlineReservationsPage() {
const load = useCallback(async () => {
try {
setLoading(true)
const result = await apiFetch<OnlineReservation[]>('/reservations?source=MARKETPLACE&status=DRAFT&pageSize=100')
const result = await apiFetch<OnlineReservation[]>('/reservations?source=CARPLACE&status=DRAFT&pageSize=100')
setRows(
(result ?? []).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
)
@@ -595,7 +595,7 @@ export default function ReservationDetailPage() {
{actionError && <div className="card p-4 text-sm text-red-600">{actionError}</div>}
{reservation.source === 'MARKETPLACE' ? (
{reservation.source === 'CARPLACE' ? (
<div className="card p-6">
<h3 className="mb-4 text-base font-semibold text-slate-900">{copy.bookingProgress}</h3>
<div className="flex flex-wrap items-center gap-2">
@@ -17,12 +17,12 @@ import { apiFetch } from '@/lib/api'
import { useDashboardI18n } from '@/components/I18nProvider'
import { getMoroccanCityOptions } from '@/lib/moroccanCities'
type SectionKey = 'company' | 'storefront' | 'payments' | 'rental-policies' | 'insurance' | 'pricing' | 'accounting'
type SectionKey = 'company' | 'carplace' | 'payments' | 'rental-policies' | 'insurance' | 'pricing' | 'accounting'
type FeatureKey =
| 'settings.company_profile'
| 'settings.public_contact'
| 'settings.locale_currency'
| 'settings.storefront_listing'
| 'settings.carplace_listing'
| 'settings.branding_basic'
| 'settings.branding_custom'
| 'settings.branding_hero'
@@ -82,7 +82,7 @@ interface BrandSettings {
amanpaySecretKey?: string | null
defaultLocale?: string
defaultCurrency?: string
isListedOnMarketplace: boolean
isListedOnCarplace: boolean
}
interface ContractSettings {
@@ -148,12 +148,12 @@ function SettingsPageContent() {
loading: 'Loading settings...', save: 'Saving...', saved: 'Saved.', saveSection: 'Save section', currentPlan: 'Current plan',
subscriptionStatus: 'Subscription status', manageSubscription: 'Manage subscription', requiredPlan: 'Requires',
lockedTitle: 'This settings area is not included in your current plan.', lockedBody: 'Your saved configuration is preserved and becomes editable again after upgrade.',
readOnly: 'This section is read-only while your subscription access is restricted.', companyHint: 'Default language affects storefront text and generated contracts when available.',
publicProfile: 'Public profile', storefrontBasics: 'Storefront basics', premiumBranding: 'Available on GROWTH',
readOnly: 'This section is read-only while your subscription access is restricted.', companyHint: 'Default language affects Carplace text and generated contracts when available.',
publicProfile: 'Public profile', carplaceBasics: 'Carplace basics', premiumBranding: 'Available on GROWTH',
paymentsBody: 'Configure renter payment providers. Secret values are never shown after saving.',
policies: 'Fuel and damage policies', additionalDriver: 'Additional-driver automation', insuranceNew: 'New insurance policy',
pricingNew: 'New pricing rule', accountingSchedule: 'Scheduled delivery is available on PRO.', noItems: 'No records configured yet.',
marketplace: 'Listed on storefront', logo: 'Logo', hero: 'Hero image', upload: 'Upload', active: 'Active', inactive: 'Inactive',
carplace: 'Listed on Carplace', logo: 'Logo', hero: 'Hero image', upload: 'Upload', active: 'Active', inactive: 'Inactive',
labels: {
displayName: 'Display name', tagline: 'Tagline', publicEmail: 'Public email', publicPhone: 'Public phone', whatsapp: 'WhatsApp number',
city: 'City', country: 'Country', websiteUrl: 'Website URL', defaultLocale: 'Default language', defaultCurrency: 'Default currency',
@@ -169,11 +169,11 @@ function SettingsPageContent() {
subscriptionStatus: 'Statut abonnement', manageSubscription: 'Gérer labonnement', requiredPlan: 'Requiert',
lockedTitle: 'Cette section nest pas incluse dans votre plan actuel.', lockedBody: 'La configuration enregistrée est conservée et redevient modifiable après mise à niveau.',
readOnly: 'Cette section est en lecture seule pendant la restriction daccès.', companyHint: 'La langue par défaut affecte la vitrine et les contrats générés si disponibles.',
publicProfile: 'Profil public', storefrontBasics: 'Bases de la vitrine', premiumBranding: 'Disponible avec GROWTH',
publicProfile: 'Profil public', carplaceBasics: 'Paramètres Carplace', premiumBranding: 'Disponible avec GROWTH',
paymentsBody: 'Configurez les prestataires de paiement. Les secrets ne sont jamais affichés après enregistrement.',
policies: 'Carburant et dommages', additionalDriver: 'Automatisation conducteur additionnel', insuranceNew: 'Nouvelle police',
pricingNew: 'Nouvelle règle', accountingSchedule: 'Lenvoi planifié est disponible avec PRO.', noItems: 'Aucun enregistrement.',
marketplace: 'Publié sur la vitrine', logo: 'Logo', hero: 'Image principale', upload: 'Téléverser', active: 'Actif', inactive: 'Inactif',
carplace: 'Publié sur Carplace', logo: 'Logo', hero: 'Image principale', upload: 'Téléverser', active: 'Actif', inactive: 'Inactif',
labels: {
displayName: 'Nom affiché', tagline: 'Slogan', publicEmail: 'E-mail public', publicPhone: 'Téléphone public', whatsapp: 'Numéro WhatsApp',
city: 'Ville', country: 'Pays', websiteUrl: 'Site web', defaultLocale: 'Langue par défaut', defaultCurrency: 'Devise par défaut',
@@ -189,11 +189,11 @@ function SettingsPageContent() {
subscriptionStatus: 'حالة الاشتراك', manageSubscription: 'إدارة الاشتراك', requiredPlan: 'يتطلب',
lockedTitle: 'هذا القسم غير مشمول في خطتك الحالية.', lockedBody: 'يتم الاحتفاظ بالإعدادات المحفوظة وتعود قابلة للتعديل بعد الترقية.',
readOnly: 'هذا القسم للقراءة فقط أثناء تقييد الوصول.', companyHint: 'تؤثر اللغة الافتراضية على الواجهة والعقود عند توفرها.',
publicProfile: 'الملف العام', storefrontBasics: 'أساسيات الواجهة', premiumBranding: 'متاح في GROWTH',
publicProfile: 'الملف العام', carplaceBasics: 'أساسيات الواجهة', premiumBranding: 'متاح في GROWTH',
paymentsBody: 'إعداد مزودي دفع المستأجرين. لا يتم عرض المفاتيح السرية بعد الحفظ.',
policies: 'سياسات الوقود والأضرار', additionalDriver: 'أتمتة السائق الإضافي', insuranceNew: 'سياسة تأمين جديدة',
pricingNew: 'قاعدة تسعير جديدة', accountingSchedule: 'الإرسال المجدول متاح في PRO.', noItems: 'لا توجد سجلات بعد.',
marketplace: 'مدرج في الواجهة', logo: 'الشعار', hero: 'صورة الواجهة', upload: 'رفع', active: 'نشط', inactive: 'غير نشط',
carplace: 'مدرج على Carplace', logo: 'الشعار', hero: 'صورة الواجهة', upload: 'رفع', active: 'نشط', inactive: 'غير نشط',
labels: {
displayName: 'اسم العرض', tagline: 'الشعار', publicEmail: 'البريد العام', publicPhone: 'الهاتف العام', whatsapp: 'رقم واتساب',
city: 'المدينة', country: 'الدولة', websiteUrl: 'الموقع', defaultLocale: 'اللغة الافتراضية', defaultCurrency: 'العملة الافتراضية',
@@ -253,7 +253,7 @@ function SettingsPageContent() {
useEffect(() => {
async function loadSection() {
try {
if ((activeSection === 'company' || activeSection === 'storefront' || activeSection === 'payments') && !brand) {
if ((activeSection === 'company' || activeSection === 'carplace' || activeSection === 'payments') && !brand) {
setBrand(await apiFetch<BrandSettings | null>('/companies/me/brand'))
}
if (activeSection === 'rental-policies' && !contractSettings) {
@@ -295,7 +295,7 @@ function SettingsPageContent() {
publicCity: brand.publicCity || undefined, publicCountry: brand.publicCountry || undefined,
websiteUrl: brand.websiteUrl || undefined, defaultLocale: brand.defaultLocale || undefined,
whatsappNumber: brand.whatsappNumber || undefined, defaultCurrency: brand.defaultCurrency || undefined,
isListedOnMarketplace: brand.isListedOnMarketplace,
isListedOnCarplace: brand.isListedOnCarplace,
amanpayMerchantId: canEdit('settings.renter_payments') ? brand.amanpayMerchantId || undefined : undefined,
amanpaySecretKey: canEdit('settings.renter_payments') ? brand.amanpaySecretKey || undefined : undefined,
paypalEmail: canEdit('settings.renter_payments') ? brand.paypalEmail || undefined : undefined,
@@ -471,11 +471,11 @@ function SettingsPageContent() {
</SectionCard>
)}
{activeSection === 'storefront' && brand && (
{activeSection === 'carplace' && brand && (
<SectionCard onSave={saveBrand} saving={saving} copy={copy}>
<label className="flex items-center gap-2 text-sm font-medium text-slate-700">
<input type="checkbox" checked={brand.isListedOnMarketplace} disabled={!canEdit('settings.storefront_listing')} onChange={(e) => setBrand({ ...brand, isListedOnMarketplace: e.target.checked })} />
{copy.marketplace}
<input type="checkbox" checked={brand.isListedOnCarplace} disabled={!canEdit('settings.carplace_listing')} onChange={(e) => setBrand({ ...brand, isListedOnCarplace: e.target.checked })} />
{copy.carplace}
</label>
<div className="mt-5 grid gap-4 lg:grid-cols-2">
<AssetField label={copy.logo} value={brand.logoUrl} disabled={!canEdit('settings.branding_basic')} uploading={uploadingAsset === 'logo'} copy={copy} onChange={(file) => uploadBrandAsset('logo', file)} />
@@ -129,7 +129,7 @@ export default function SubscriptionPage() {
statusLabels: { TRIALING: 'Trialing', ACTIVE: 'Active', PAST_DUE: 'Past due', CANCELLED: 'Cancelled', CANCELED: 'Canceled', UNPAID: 'Unpaid', EXPIRED: 'Expired', SUSPENDED: 'Suspended' } as Record<string, string>,
invoiceStatusLabels: { PAID: 'Paid', PENDING: 'Pending', FAILED: 'Failed', REFUNDED: 'Refunded' } as Record<string, string>,
planFeatures: {
STARTER: ['Up to 10 vehicles', '1 user seat', 'Basic analytics', 'Storefront listing'],
STARTER: ['Up to 10 vehicles', '1 user seat', 'Basic analytics', 'Carplace listing'],
GROWTH: ['Up to 50 vehicles', '5 user seats', 'Full analytics', 'Priority listing', 'Custom branding'],
PRO: ['Unlimited vehicles', 'Unlimited seats', 'Advanced reports', 'API access', 'Dedicated support'],
} as Record<Plan, string[]>,
@@ -177,7 +177,7 @@ export default function SubscriptionPage() {
statusLabels: { TRIALING: 'Essai', ACTIVE: 'Actif', PAST_DUE: 'En retard', CANCELLED: 'Annulé', CANCELED: 'Annulé', UNPAID: 'Impayé', EXPIRED: 'Expiré', SUSPENDED: 'Suspendu' } as Record<string, string>,
invoiceStatusLabels: { PAID: 'Payé', PENDING: 'En attente', FAILED: 'Échec', REFUNDED: 'Remboursé' } as Record<string, string>,
planFeatures: {
STARTER: ['Jusqu’à 10 véhicules', '1 utilisateur', 'Analyses de base', 'Présence storefront'],
STARTER: ['Jusqu’à 10 véhicules', '1 utilisateur', 'Analyses de base', 'Présence sur Carplace'],
GROWTH: ['Jusqu’à 50 véhicules', '5 utilisateurs', 'Analyses complètes', 'Mise en avant prioritaire', 'Personnalisation'],
PRO: ['Véhicules illimités', 'Utilisateurs illimités', 'Rapports avancés', 'Accès API', 'Support dédié'],
} as Record<Plan, string[]>,
@@ -6,7 +6,7 @@ import { useState } from "react";
import { useDashboardI18n } from "@/components/I18nProvider";
import PublicShell from "@/components/layout/PublicShell";
import { API_BASE } from "@/lib/api";
import { storefrontUrl } from "@/lib/urls";
import { carplaceUrl } from "@/lib/urls";
const DASHBOARD_LOGO_SRC = "/dashboard/rentaldrivego.png";
@@ -97,7 +97,7 @@ export default function ForgotPasswordPageClient({
<div className="w-full max-w-md">
<div className="mb-8 text-center">
<div className="flex justify-center">
<a href={storefrontUrl} target="_top">
<a href={carplaceUrl} target="_top">
<Image
src={DASHBOARD_LOGO_SRC}
alt="RentalDriveGo"
+7 -7
View File
@@ -5,7 +5,7 @@ import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { apiFetch } from '@/lib/api'
import PublicShell from '@/components/layout/PublicShell'
import { storefrontUrl } from '@/lib/urls'
import { carplaceUrl } from '@/lib/urls'
type Step = 1 | 2 | 3
@@ -28,7 +28,7 @@ export default function OnboardingPage() {
amanpayMerchantId: '',
amanpaySecretKey: '',
paypalEmail: '',
isListedOnMarketplace: true,
isListedOnCarplace: true,
})
async function handleStep1() {
@@ -81,7 +81,7 @@ export default function OnboardingPage() {
amanpayMerchantId: payments.amanpayMerchantId || undefined,
amanpaySecretKey: payments.amanpaySecretKey || undefined,
paypalEmail: payments.paypalEmail || undefined,
isListedOnMarketplace: payments.isListedOnMarketplace,
isListedOnCarplace: payments.isListedOnCarplace,
}),
})
router.push('/')
@@ -97,7 +97,7 @@ export default function OnboardingPage() {
<main className="flex items-center justify-center px-4 py-12">
<div className="w-full max-w-lg">
<div className="mb-8 text-center">
<a href={storefrontUrl} className="text-xs font-semibold uppercase tracking-widest text-blue-600">RentalDriveGo</a>
<a href={carplaceUrl} className="text-xs font-semibold uppercase tracking-widest text-blue-600">RentalDriveGo</a>
<h1 className="mt-2 text-3xl font-bold text-slate-900">Set up your account</h1>
<p className="mt-1 text-sm text-slate-500">Step {step} of 3</p>
</div>
@@ -268,10 +268,10 @@ export default function OnboardingPage() {
<input
type="checkbox"
className="h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500"
checked={payments.isListedOnMarketplace}
onChange={(e) => setPayments({ ...payments, isListedOnMarketplace: e.target.checked })}
checked={payments.isListedOnCarplace}
onChange={(e) => setPayments({ ...payments, isListedOnCarplace: e.target.checked })}
/>
<span className="text-sm text-slate-700">List my company on the RentalDriveGo storefront</span>
<span className="text-sm text-slate-700">List my company on the RentalDriveGo Carplace</span>
</label>
<div className="flex gap-3">
<button onClick={() => setStep(2)} className="btn-secondary flex-1 justify-center">Back</button>
@@ -6,7 +6,7 @@ import { useEffect, useRef, useState } from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useDashboardI18n } from "@/components/I18nProvider";
import PublicShell from "@/components/layout/PublicShell";
import { adminUrl, storefrontUrl } from "@/lib/urls";
import { adminUrl, websiteUrl } from "@/lib/urls";
import { API_BASE, EMPLOYEE_PROFILE_KEY } from "@/lib/api";
import { toPublicDashboardPath } from "@/lib/dashboardPaths";
@@ -177,7 +177,7 @@ export default function SignInPageClient({
<div className="w-full max-w-md">
<div className="mb-8 text-center">
<div className="flex justify-center">
<a href={storefrontUrl} target="_top">
<a href={websiteUrl} target="_top">
<Image
src={DASHBOARD_LOGO_SRC}
alt="RentalDriveGo"
@@ -6,7 +6,7 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { apiFetch } from "@/lib/api";
import PublicShell from "@/components/layout/PublicShell";
import { useDashboardI18n } from "@/components/I18nProvider";
import { storefrontUrl } from "@/lib/urls";
import { websiteUrl } from "@/lib/urls";
const DASHBOARD_LOGO_SRC = "/dashboard/rentaldrivego.png";
@@ -281,7 +281,7 @@ export default function SignUpForm({
<div className="w-full max-w-md">
<div className="mb-8 text-center">
<div className="flex justify-center">
<a href={storefrontUrl} target="_top">
<a href={websiteUrl} target="_top">
<Image
src={DASHBOARD_LOGO_SRC}
alt="RentalDriveGo"