sync: Auto-sync from ACG-M-L5090 at 2026-03-10 19:11:00
Synced files: - Quote wizard frontend (all components, hooks, types, config) - API updates (config, models, routers, schemas, services) - Client work (bg-builders, gurushow) - Scripts (BGB Lesley termination, CIPP, Datto, migration) - Temp files (Bardach contacts, VWP investigation, misc) - Credentials and session logs - Email service, PHP API, session logs Machine: ACG-M-L5090 Timestamp: 2026-03-10 19:11:00 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -60,27 +60,50 @@ export function Step4VoIP({
|
||||
transition={{ duration: 0.3 }}
|
||||
className="space-y-6"
|
||||
>
|
||||
{/* Service Explainer */}
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm sm:text-base text-gray-500 leading-relaxed">
|
||||
<strong className="text-[#333d49]">VoIP (Voice over IP)</strong> replaces traditional
|
||||
phone lines with a modern cloud-based phone system. Your calls travel over the internet,
|
||||
which means lower costs, more features, and the flexibility to take calls from your
|
||||
desk phone, computer, or mobile app — anywhere with an internet connection.
|
||||
</p>
|
||||
<p className="text-sm text-gray-400 leading-relaxed">
|
||||
Every plan includes unlimited local and long-distance calling, auto-attendant (press 1
|
||||
for sales, etc.), voicemail-to-email, call forwarding, and the ability to keep your
|
||||
existing phone numbers. Higher tiers add call recording, analytics, CRM integrations,
|
||||
and video conferencing. We can also provide desk phones and headsets as a purchase or
|
||||
monthly rental.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* VoIP Toggle */}
|
||||
<div className="bg-gray-50 rounded-lg p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Phone className="w-6 h-6 text-[#fe7400]" />
|
||||
<div>
|
||||
<h3 className="font-semibold text-[#333d49]">Do you need business phones?</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
<div className="bg-[#f8f9fb] rounded-xl p-4 sm:p-5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className="w-9 h-9 sm:w-10 sm:h-10 rounded-xl bg-[#fe7400]/8 flex items-center justify-center flex-shrink-0">
|
||||
<Phone className="w-4 h-4 sm:w-5 sm:h-5 text-[#fe7400]" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h3 className="font-bold text-[#333d49] text-sm sm:text-base"
|
||||
style={{ fontFamily: "'Plus Jakarta Sans', sans-serif" }}>
|
||||
Do you need business phones?
|
||||
</h3>
|
||||
<p className="text-xs sm:text-sm text-gray-400">
|
||||
Modern VoIP phone system with advanced features
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<label className="relative inline-flex items-center cursor-pointer flex-shrink-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={voipSelection.enabled}
|
||||
onChange={(e) => onSetVoIPEnabled(e.target.checked)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-14 h-7 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[#fe7400]/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-6 after:w-6 after:transition-all peer-checked:bg-[#fe7400]"></div>
|
||||
<span className="ml-3 text-sm font-medium text-gray-700">
|
||||
<div className="w-14 h-7 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[#fe7400]/10 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-6 after:w-6 after:transition-all peer-checked:bg-[#fe7400]"></div>
|
||||
<span className="ml-3 text-sm font-semibold text-gray-500"
|
||||
style={{ fontFamily: "'Plus Jakarta Sans', sans-serif" }}>
|
||||
{voipSelection.enabled ? 'Yes' : 'No'}
|
||||
</span>
|
||||
</label>
|
||||
@@ -97,19 +120,22 @@ export function Step4VoIP({
|
||||
className="space-y-6"
|
||||
>
|
||||
{/* User Count */}
|
||||
<div className="flex items-center gap-4 p-4 border border-gray-200 rounded-lg">
|
||||
<label className="text-sm font-medium text-[#333d49]">Number of phone users:</label>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 p-4 border border-gray-200/80 rounded-xl bg-white shadow-card">
|
||||
<label className="text-sm font-medium text-[#333d49]"
|
||||
style={{ fontFamily: "'Plus Jakarta Sans', sans-serif" }}>
|
||||
Number of phone users:
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={voipSelection.userCount}
|
||||
onChange={(e) => onSetVoIPUserCount(parseInt(e.target.value, 10) || 1)}
|
||||
className="w-24"
|
||||
className="w-full sm:w-24"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tier Selection */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
|
||||
{voipTiers.map((tier, index) => {
|
||||
const isSelected = voipSelection.tierId === tier.id;
|
||||
const monthlyPrice = tier.pricePerUser * voipSelection.userCount;
|
||||
@@ -120,43 +146,48 @@ export function Step4VoIP({
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
whileHover={{ y: -4 }}
|
||||
whileHover={{ y: -3 }}
|
||||
>
|
||||
<Card
|
||||
variant={isSelected ? 'highlighted' : tier.recommended ? 'elevated' : 'default'}
|
||||
variant={isSelected ? 'highlighted' : 'default'}
|
||||
padding="none"
|
||||
className={`relative overflow-hidden cursor-pointer h-full ${
|
||||
tier.recommended && !isSelected ? 'ring-2 ring-[#333d49]' : ''
|
||||
tier.recommended && !isSelected ? 'ring-2 ring-[#fe7400]/30' : ''
|
||||
}`}
|
||||
onClick={() => onSetVoIPTier(tier.id)}
|
||||
>
|
||||
{tier.recommended && (
|
||||
<div className="absolute top-0 right-0">
|
||||
<div className="bg-[#fe7400] text-white text-xs font-semibold px-2 py-1 rounded-bl-lg">
|
||||
Popular
|
||||
</div>
|
||||
<div className="bg-gradient-accent text-white text-[11px] font-bold px-3 py-1.5 text-center uppercase tracking-wider"
|
||||
style={{ fontFamily: "'Plus Jakarta Sans', sans-serif" }}>
|
||||
Popular
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-4">
|
||||
<h3 className="text-lg font-semibold text-[#333d49]">{tier.name}</h3>
|
||||
<p className="text-xs text-gray-500 mb-3">{tier.description}</p>
|
||||
<h3 className="text-base font-bold text-[#333d49]"
|
||||
style={{ fontFamily: "'Plus Jakarta Sans', sans-serif" }}>
|
||||
{tier.name}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-400 mb-3">{tier.description}</p>
|
||||
|
||||
<div className="mb-3">
|
||||
<span className="text-xl font-bold text-[#333d49]">
|
||||
<span className="text-xl font-bold text-[#333d49]"
|
||||
style={{ fontFamily: "'Plus Jakarta Sans', sans-serif" }}>
|
||||
{formatCurrency(monthlyPrice)}
|
||||
</span>
|
||||
<span className="text-gray-500 text-xs">/mo</span>
|
||||
<span className="text-gray-400 text-xs">/mo</span>
|
||||
<p className="text-xs text-gray-400">
|
||||
{formatCurrency(tier.pricePerUser)}/user
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ul className="space-y-1 mb-4">
|
||||
<ul className="space-y-1.5 mb-4">
|
||||
{tier.features.slice(0, 3).map((feature, idx) => (
|
||||
<li key={idx} className="flex items-start gap-1.5 text-xs">
|
||||
<Check className="w-3 h-3 text-[#fe7400] mt-0.5 flex-shrink-0" />
|
||||
<span className="text-gray-600">{feature}</span>
|
||||
<li key={idx} className="flex items-start gap-2 text-xs">
|
||||
<div className="w-3.5 h-3.5 rounded-full bg-[#fe7400]/10 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<Check className="w-2 h-2 text-[#fe7400]" strokeWidth={3} />
|
||||
</div>
|
||||
<span className="text-gray-500">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -176,19 +207,21 @@ export function Step4VoIP({
|
||||
</div>
|
||||
|
||||
{/* Hardware Section */}
|
||||
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
||||
<div className="border border-gray-200/80 rounded-xl overflow-hidden bg-white shadow-card">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowHardware(!showHardware)}
|
||||
className="w-full flex items-center justify-between p-4 bg-gray-50 hover:bg-gray-100 transition-colors"
|
||||
className="w-full flex items-center justify-between p-4 bg-[#f8f9fb] hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Headphones className="w-5 h-5 text-[#fe7400]" />
|
||||
<span className="font-medium text-[#333d49]">
|
||||
<span className="font-semibold text-[#333d49]"
|
||||
style={{ fontFamily: "'Plus Jakarta Sans', sans-serif" }}>
|
||||
Phone Hardware (Optional)
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">
|
||||
<span className="text-sm text-gray-400 font-medium"
|
||||
style={{ fontFamily: "'Plus Jakarta Sans', sans-serif" }}>
|
||||
{showHardware ? 'Hide' : 'Show'} options
|
||||
</span>
|
||||
</button>
|
||||
@@ -209,81 +242,84 @@ export function Step4VoIP({
|
||||
return (
|
||||
<div
|
||||
key={hardware.id}
|
||||
className={`p-4 rounded-lg border-2 transition-all ${
|
||||
className={`p-4 rounded-xl border-2 transition-all duration-200 ${
|
||||
isSelected
|
||||
? 'border-[#fe7400] bg-[#fe7400]/5'
|
||||
: 'border-gray-200'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-[#333d49]">{hardware.name}</h4>
|
||||
<p className="text-sm text-gray-500">{hardware.description}</p>
|
||||
<div className="flex gap-4 mt-2 text-sm">
|
||||
<span className="text-[#333d49]">
|
||||
Buy: <strong>{formatCurrency(hardware.oneTimePrice)}</strong>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<h4 className="font-semibold text-[#333d49] text-sm sm:text-base"
|
||||
style={{ fontFamily: "'Plus Jakarta Sans', sans-serif" }}>
|
||||
{hardware.name}
|
||||
</h4>
|
||||
<p className="text-xs sm:text-sm text-gray-400">{hardware.description}</p>
|
||||
<div className="flex gap-3 sm:gap-4 mt-2 text-xs sm:text-sm">
|
||||
<span className="text-gray-500">
|
||||
Buy: <strong className="text-[#333d49]">{formatCurrency(hardware.oneTimePrice)}</strong>
|
||||
</span>
|
||||
<span className="text-[#333d49]">
|
||||
Rent: <strong>{formatCurrency(hardware.monthlyRental)}</strong>/mo
|
||||
<span className="text-gray-500">
|
||||
Rent: <strong className="text-[#333d49]">{formatCurrency(hardware.monthlyRental)}</strong>/mo
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isSelected ? (
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Rental Toggle */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onAddHardware(hardware.id, selection.quantity, false)}
|
||||
className={`px-2 py-1 text-xs rounded ${
|
||||
className={`px-2.5 py-1.5 text-xs rounded-lg font-semibold transition-colors ${
|
||||
!selection.isRental
|
||||
? 'bg-[#fe7400] text-white'
|
||||
: 'bg-gray-200 text-gray-600'
|
||||
: 'bg-gray-100 text-gray-500 hover:bg-gray-200'
|
||||
}`}
|
||||
style={{ fontFamily: "'Plus Jakarta Sans', sans-serif" }}
|
||||
>
|
||||
Buy
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onAddHardware(hardware.id, selection.quantity, true)}
|
||||
className={`px-2 py-1 text-xs rounded ${
|
||||
className={`px-2.5 py-1.5 text-xs rounded-lg font-semibold transition-colors ${
|
||||
selection.isRental
|
||||
? 'bg-[#fe7400] text-white'
|
||||
: 'bg-gray-200 text-gray-600'
|
||||
: 'bg-gray-100 text-gray-500 hover:bg-gray-200'
|
||||
}`}
|
||||
style={{ fontFamily: "'Plus Jakarta Sans', sans-serif" }}
|
||||
>
|
||||
Rent
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Quantity */}
|
||||
<div className="flex items-center gap-2 border border-gray-300 rounded-lg">
|
||||
<div className="flex items-center gap-1 border border-gray-200 rounded-lg">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleQuantityChange(hardware.id, -1)}
|
||||
className="p-2 hover:bg-gray-100 rounded-l-lg"
|
||||
className="p-2 hover:bg-gray-50 rounded-l-lg transition-colors"
|
||||
disabled={selection.quantity <= 1}
|
||||
>
|
||||
<Minus className="w-4 h-4" />
|
||||
<Minus className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<span className="w-8 text-center font-medium">
|
||||
<span className="w-8 text-center font-semibold text-sm"
|
||||
style={{ fontFamily: "'Plus Jakarta Sans', sans-serif" }}>
|
||||
{selection.quantity}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleQuantityChange(hardware.id, 1)}
|
||||
className="p-2 hover:bg-gray-100 rounded-r-lg"
|
||||
className="p-2 hover:bg-gray-50 rounded-r-lg transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Remove */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onRemoveHardware(hardware.id)}
|
||||
className="p-2 text-red-500 hover:bg-red-50 rounded-lg"
|
||||
className="p-2 text-red-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors ml-auto"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
@@ -317,21 +353,29 @@ export function Step4VoIP({
|
||||
|
||||
{/* Info */}
|
||||
<ExpandableInfo title="VoIP Features & Benefits">
|
||||
<ul className="space-y-2">
|
||||
<li className="flex items-start gap-2">
|
||||
<Check className="w-4 h-4 text-[#fe7400] mt-0.5 flex-shrink-0" />
|
||||
<ul className="space-y-2.5">
|
||||
<li className="flex items-start gap-2.5">
|
||||
<div className="w-4 h-4 rounded-full bg-[#fe7400]/10 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<Check className="w-2.5 h-2.5 text-[#fe7400]" strokeWidth={3} />
|
||||
</div>
|
||||
<span>Unlimited local and long-distance calling</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Check className="w-4 h-4 text-[#fe7400] mt-0.5 flex-shrink-0" />
|
||||
<li className="flex items-start gap-2.5">
|
||||
<div className="w-4 h-4 rounded-full bg-[#fe7400]/10 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<Check className="w-2.5 h-2.5 text-[#fe7400]" strokeWidth={3} />
|
||||
</div>
|
||||
<span>Mobile apps for iOS and Android - take calls anywhere</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Check className="w-4 h-4 text-[#fe7400] mt-0.5 flex-shrink-0" />
|
||||
<li className="flex items-start gap-2.5">
|
||||
<div className="w-4 h-4 rounded-full bg-[#fe7400]/10 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<Check className="w-2.5 h-2.5 text-[#fe7400]" strokeWidth={3} />
|
||||
</div>
|
||||
<span>Auto-attendant and professional voicemail</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Check className="w-4 h-4 text-[#fe7400] mt-0.5 flex-shrink-0" />
|
||||
<li className="flex items-start gap-2.5">
|
||||
<div className="w-4 h-4 rounded-full bg-[#fe7400]/10 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<Check className="w-2.5 h-2.5 text-[#fe7400]" strokeWidth={3} />
|
||||
</div>
|
||||
<span>Keep your existing phone numbers</span>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -339,18 +383,19 @@ export function Step4VoIP({
|
||||
|
||||
{/* Totals */}
|
||||
<div className="space-y-3">
|
||||
<div className="bg-[#333d49] text-white rounded-lg p-5 flex items-center justify-between">
|
||||
<span className="text-lg">VoIP Monthly Total</span>
|
||||
<span className="text-3xl font-bold">
|
||||
<div className="bg-gradient-dark text-white rounded-xl p-4 sm:p-5 flex items-center justify-between gap-3">
|
||||
<span className="text-sm sm:text-base font-medium opacity-90">VoIP Monthly Total</span>
|
||||
<span className="text-2xl sm:text-3xl font-bold whitespace-nowrap" style={{ fontFamily: "'Plus Jakarta Sans', sans-serif" }}>
|
||||
{formatCurrency(getVoIPMonthly())}
|
||||
<span className="text-lg font-normal opacity-75">/month</span>
|
||||
<span className="text-xs sm:text-sm font-medium opacity-60 ml-1">/mo</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{getVoIPOneTime() > 0 && (
|
||||
<div className="bg-gray-100 rounded-lg p-4 flex items-center justify-between">
|
||||
<span className="text-gray-700">Hardware Purchase (One-Time)</span>
|
||||
<span className="text-xl font-bold text-[#333d49]">
|
||||
<div className="bg-[#f8f9fb] rounded-xl p-4 flex items-center justify-between border border-gray-200/80">
|
||||
<span className="text-gray-500 font-medium">Hardware Purchase (One-Time)</span>
|
||||
<span className="text-xl font-bold text-[#333d49]"
|
||||
style={{ fontFamily: "'Plus Jakarta Sans', sans-serif" }}>
|
||||
{formatCurrency(getVoIPOneTime())}
|
||||
</span>
|
||||
</div>
|
||||
@@ -364,10 +409,10 @@ export function Step4VoIP({
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="text-center py-8 text-gray-500"
|
||||
className="text-center py-12 text-gray-400"
|
||||
>
|
||||
<Phone className="w-12 h-12 mx-auto mb-3 opacity-30" />
|
||||
<p>You can always add VoIP services later.</p>
|
||||
<Phone className="w-12 h-12 mx-auto mb-3 opacity-20" />
|
||||
<p className="text-sm">You can always add VoIP services later.</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
Reference in New Issue
Block a user