PageSourceSearch

https://www.pourify.com/assets/ZAROICalculator-B5F7DIw0.js

js pourify.com collected 2026-10-04 19:37:15 UTC 21,393 bytes, 473 lines download raw bytes

1const n=`import { useState, forwardRef } from "react";
2import { Link } from "react-router-dom";
3import { useTranslation } from "react-i18next";
4import { Slider } from "@/components/ui/slider";
5import { Button } from "@/components/ui/button";
6import { Card, CardContent } from "@/components/ui/card";
7import { Input } from "@/components/ui/input";
8import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
9import { Info, Mail, Loader2, CheckCircle2 } from "lucide-react";
10import { toast } from "sonner";
11import { supabase } from "@/integrations/supabase/client";
12import { trackROIReport } from "@/lib/gtm";
13import { useLanguage } from "@/hooks/useLanguage";
14import bottlesIllustration from "@/assets/bottles-illustration.png";
15import {
16  ZA_SETUP_FEE,
17  ZA_COMMITMENT_MONTHS,
18  calcZaMonthly,
19  recommendedHubs,
20  fmtZar,
21} from "@/data/za-pricing";
22
23/* ── ZA Pricing Logic ──────────────────────────────────────── */
24
25const SETUP_FEE = ZA_SETUP_FEE;
26const TOTS_PER_BOTTLE = 30;
27
28/** Calculate Pourify monthly cost - uses single source of truth in src/data/za-pricing.ts */
29const calcPourifyMonthly = (totalBrands: number): number =>
30  calcZaMonthly(totalBrands, recommendedHubs(totalBrands));
31
32/**
33 * Calculate Competitor monthly cost. Anchored to the same baseline as
34 * Pourify (R950 base + R300/extra hub) so the comparison is apples-to-apples,
35 * with a flat per-pourer mark-up on top to reflect typical competitor pricing.
36 */
37const calcCompetitorMonthly = (totalBrands: number): number => {
38  const baseCost = 950;
39  const additional = Math.max(0, totalBrands - 5) * 195;
40  const extraHubs = Math.max(0, recommendedHubs(totalBrands) - 1);
41  return baseCost + additional + extraHubs * 300;
42};
43
44const fmt = fmtZar;
45
46/* ── Component ─────────────────────────────────────────────── */
47
48const ZAROICalculator = () => {
49  const { t } = useTranslation();
50  const { localePath, lang } = useLanguage();
51
52  const [brands, setBrands] = useState(20);
53  const [bottlesPerBrand, setBottlesPerBrand] = useState(5);
54  const [pricePerTot, setPricePerTot] = useState(40);
55  const [lossPercent, setLossPercent] = useState(12);
56
57  // Calculations
58  const weeklyGross = brands * bottlesPerBrand * TOTS_PER_BOTTLE * pricePerTot;
59  const annualLoss = weeklyGross * 52 * (lossPercent / 100);
60
61  const pourifyEfficiency = 0.05;
62  const competitorEfficiency = 0.10;
63
64  const pourifyGrossSavings = annualLoss - (weeklyGross * 52 * pourifyEfficiency);
65  const competitorGrossSavings = annualLoss - (weeklyGross * 52 * competitorEfficiency);
66
67  const pourifyMonthly = calcPourifyMonthly(brands);
68  const competitorMonthly = calcCompetitorMonthly(brands);
69
70  const pourifyAnnualCost = pourifyMonthly * 12;
71  const competitorAnnualCost = competitorMonthly * 12;
72
73  const pourifyNetSavings = Math.max(0, pourifyGrossSavings - pourifyAnnualCost);
74  const competitorNetSavings = Math.max(0, competitorGrossSavings - competitorAnnualCost);
75  const pourifyDifference = pourifyNetSavings - competitorNetSavings;
76
77  return (
78    <div className="container-pourify">
79        <div className="grid lg:grid-cols-[1fr_auto] gap-10 items-start">
80          <div className="max-w-3xl">
81            <div className="mb-8">
82              <p className="text-sm font-medium text-accent mb-2">{t("roi.badge")}</p>
83              <h2 className="text-3xl md:text-5xl font-bold mb-3 text-foreground">
84                Calculate your savings
85              </h2>
86              <p className="text-muted-foreground text-lg">
87                See how much Pourify can save your venue - and how we compare to the competition.
88              </p>
89            </div>
90
91            {/* Sliders */}
92            <div className="space-y-5 mb-6">
93              <SliderRow label="Number of brands" value={brands} min={5} max={100} display={\`\${brands}\`} onChange={setBrands} />
94              <SliderRow label="Bottles poured per brand per week" value={bottlesPerBrand} min={1} max={50} display={\`\${bottlesPerBrand}\`} onChange={setBottlesPerBrand} />
95              <SliderRow label="Price per tot" value={pricePerTot} min={25} max={150} display={fmt(pricePerTot)} onChange={setPricePerTot} />
95
96              <SliderRow
97                label="Estimated current loss"
98                value={lossPercent}
99                min={5}
100                max={50}
101                display={\`\${lossPercent}%\`}
102                onChange={setLossPercent}
103                hint="Industry average: 9–15%"
104                  presets={[
105                    { label: "Bars", value: 12 },
106                    { label: "Restaurants", value: 10 },
107                    { label: "Hotels", value: 9 },
108                    { label: "Nightclubs", value: 15 },
109                  ]}
110              />
111            </div>
112
113            {/* Formula breakdown */}
114            <div className="flex flex-wrap items-center justify-center gap-2 mb-6">
115              <FormulaChip value={\`\${brands}\`} label="Brands" />
116              <span className="text-muted-foreground font-medium">×</span>
117              <FormulaChip value={\`\${bottlesPerBrand}\`} label="Bottles/wk" />
118              <span className="text-muted-foreground font-medium">×</span>
119              <FormulaChip value="30" label="Tots/bottle" />
120              <span className="text-muted-foreground font-medium">×</span>
121              <FormulaChip value={fmt(pricePerTot)} label="Per tot" />
122              <span className="text-muted-foreground font-medium">×</span>
123              <FormulaChip value={\`\${lossPercent}%\`} label="Loss" />
124              <span className="text-muted-foreground font-medium">×</span>
125              <FormulaChip value="52" label="Weeks" />
126              <span className="text-muted-foreground font-medium">=</span>
127              <FormulaChip value={fmt(annualLoss)} label="Annual loss" highlight />
128            </div>
129
130            {/* Comparison Table */}
131            <div className="rounded-xl overflow-hidden border border-border mb-6">
132              <table className="w-full text-sm">
133                <thead>
134                  <tr className="bg-foreground text-background">
135                    <th className="text-left p-3 font-medium">Metric</th>
136                    <th className="text-right p-3 font-medium">
137                      <span className="inline-flex items-center gap-1.5">
138                        <span className="w-2 h-2 rounded-full bg-accent" /> Pourify
139                      </span>
140                    </th>
141                    <th className="text-right p-3 font-medium">Competitor</th>
142                    <th className="text-right p-3 font-medium">Difference</th>
143                  </tr>
144                </thead>
145                <tbody className="divide-y divide-border">
146                  <tr className="bg-background/60">
147                    <td className="p-3 text-muted-foreground">Residual loss rate</td>
148                    <td className="p-3 text-right font-semibold text-foreground">5%</td>
149                    <td className="p-3 text-right text-foreground">10%</td>
150                    <td className="p-3 text-right text-accent font-semibold">5%</td>
151                  </tr>
152                  <tr className="bg-background/40">
153                    <td className="p-3 text-muted-foreground">Gross annual savings</td>
154                    <td className="p-3 text-right font-semibold text-foreground">{fmt(pourifyGrossSavings)}</td>
155                    <td className="p-3 text-right text-foreground">{fmt(competitorGrossSavings)}</td>
156                    <td className="p-3 text-right text-accent font-semibold">{fmt(Math.abs(pourifyGrossSavings - competitorGrossSavings))}</td>
157                  </tr>
158                  <tr className="bg-background/60">
159                    <td className="p-3 text-muted-foreground">Monthly Plan</td>
160                    <td className="p-3 text-right font-semibold text-foreground">{fmt(pourifyMonthly)}/mo</td>
161                    <td className="p-3 text-right text-foreground">{fmt(competitorMonthly)}/mo</td>
162                    <td className="p-3 text-right text-accent font-semibold">{fmt(Math.abs(pourifyMonthly - competitorMonthly))}/mo</td>
163                  </tr>
164                  <tr className="bg-background/40">
165                    <td className="p-3 text-muted-foreground">Annual cost</td>
166                    <td className="p-3 text-right font-semibold text-foreground">{fmt(pourifyAnnualCost)}/yr</td>
167                    <td className="p-3 text-right text-foreground">{fmt(competitorAnnualCost)}/yr</td>
168                    <td className="p-3 text-right text-accent font-semibold">{fmt(Math.abs(pourifyAnnualCost - competitorAnnualCost))}/yr</td>
169                  </tr>
170                  <tr className="bg-background/60">
171                    <td className="p-3 text-muted-foreground">Setup fee</td>
172                    <td className="p-3 text-right font-semibold text-foreground">{fmt(SETUP_FEE)}</td>
173                    <td className="p-3 text-right text-foreground">—</td>
174                    <td className="p-3 text-right text-muted-foreground">—</td>
175                  </tr>
176                  <tr className="bg-accent/10">
177                    <td className="p-3 font-bold text-foreground">Net annual savings</td>
178                    <td className="p-3 text-right font-bold text-accent text-lg">{fmt(pourifyNetSavings)}</td>
179                    <td className="p-3 text-right font-bold text-foreground">{fmt(competitorNetSavings)}</td>
180                    <td className="p-3 text-right font-bold text-accent text-lg">{fmt(Math.abs(pourifyDifference))}</td>
181                  </tr>
182                </tbody>
183              </table>
184            </div>
185
186            {/* Highlight cards */}
187            <div className="grid sm:grid-cols-3 gap-3 mb-6">
188              <div className="rounded-xl bg-primary text-primary-foreground p-5 text-center">
189                <p className="text-xs opacity-70 mb-1">Your annual loss</p>
190                <p className="text-3xl font-bold text-accent">{fmt(annualLoss)}</p>
191              </div>
192              <div className="rounded-xl bg-primary text-primary-foreground p-5 text-center">
193                <p className="text-xs opacity-70 mb-1">Net savings with Pourify</p>
194                <p className="text-3xl font-bold text-accent">{fmt(pourifyNetSavings)}</p>
195                <p className="text-xs opacity-70 mt-1">per year</p>
196              </div>
197              <div className="rounded-xl bg-primary text-primary-foreground p-5 text-center">
198                <p className="text-xs opacity-70 mb-1">Pourify advantage</p>
199                <p className="text-3xl font-bold text-accent">{fmt(pourifyDifference)}</p>
200                <p className="text-xs opacity-70 mt-1">vs. competitor</p>
201              </div>
202            </div>
203
204            {/* Pricing tiers */}
205            <div className="mb-6">
206              <h3 className="text-lg font-bold mb-2 text-foreground">Monthly Plan (ex VAT)</h3>
207              <p className="text-sm text-muted-foreground mb-4">
208                Your tier for {brands} pourers: <span className="font-bold text-foreground">{fmt(pourifyMonthly)}/month</span>
209                {" "}+ once-off setup fee of <span className="font-bold text-foreground">{fmt(SETUP_FEE)}</span>.{" "}
210                {ZA_COMMITMENT_MONTHS}-month minimum term.
211              </p>
212            </div>
213
214            {/* CTA + Email capture */}
215            <div className="text-center space-y-3">
216              <div className="flex flex-col sm:flex-row gap-3 justify-center">
217                <Button asChild size="lg" variant="accent" className="px-8">
218                  <Link to={localePath("/pricing")}>View pricing</Link>
219                </Button>
220                <Button asChild size="lg" variant="outline" className="px-8">
221                  <Link to={localePath("/demo")}>Book a Demo</Link>
222                </Button>
223              </div>
224            </div>
225
226            {/* ROI Report email capture - also serves as Request Quote */}
227            <ZAReportCapture
228              brands={brands}
229              bottlesPerBrand={bottlesPerBrand}
230              pricePerTot={pricePerTot}
231              lossPercent={lossPercent}
232              annualLoss={annualLoss}
233              pourifyMonthly={pourifyMonthly}
234              pourifyNetSavings={pourifyNetSavings}
235              competitorMonthly={competitorMonthly}
236              competitorNetSavings={competitorNetSavings}
237              pourifyDifference={pourifyDifference}
238              language={lang}
239            />
240          </div>
241
242          {/* Illustration */}
243          <div className="hidden lg:block w-[340px] xl:w-[400px] shrink-0 sticky top-28">
244            <img src={bottlesIllustration} alt="Bar bottles illustration" loading="lazy" className="w-full h-auto rounded-2xl" />
245          </div>
246        </div>
247      </div>
248  );
249};
250
251/* ── Shared sub-components ─────────────────────────────────── */
252
253const SliderRow = ({
254  label, value, min, max, display, onChange, hint, presets,
255}: {
256  label: string; value: number; min: number; max: number; display: string;
257  onChange: (v: number) => void; hint?: string;
258  presets?: { label: string; value: number }[];
259}) => (
260  <div>
261    <div className="flex justify-between mb-1">
262      <span className="text-sm font-medium text-foreground">{label}</span>
263      <span className="text-2xl font-bold text-foreground">{display}</span>
264    </div>
265    {hint && <p className="text-xs text-muted-foreground mb-1">{hint}</p>}
266    {presets && presets.length > 0 && (
267      <div className="flex flex-wrap gap-1.5 mb-2">
268        {presets.map((p) => (
269          <button
270            key={p.label}
271            onClick={() => onChange(p.value)}
272            className={\`px-2.5 py-1 rounded-full text-xs font-medium border transition-all \${
273              value === p.value
274                ? "border-primary bg-primary/10 text-primary"
275                : "border-border hover:border-primary/50 text-muted-foreground hover:text-foreground"
276            }\`}
277          >
278            {p.label} ({p.value}%)
279          </button>
280        ))}
281      </div>
282    )}
283    <Slider value={[value]} min={min} max={max} step={1} onValueChange={([v]) => onChange(v)} />
284  </div>
285);
286
287const FormulaChip = ({ value, label, highlight }: { value: string; label: string; highlight?: boolean }) => (
288  <div className="flex flex-col items-center">
289    <span className={\`px-3 py-1.5 rounded-lg text-sm font-semibold \${highlight ? "bg-foreground text-background" : "bg-background/60 text-foreground"}\`}>
290      {value}
291    </span>
292    <span className="text-[10px] text-muted-foreground mt-1 text-center max-w-[80px]">{label}</span>
293  </div>
294);
295
296/* ── ZA Email Capture + Quote Request ──────────────────────── */
297
298interface ZAReportCaptureProps {
299  brands: number;
300  bottlesPerBrand: number;
301  pricePerTot: number;
302  lossPercent: number;
303  annualLoss: number;
304  pourifyMonthly: number;
305  pourifyNetSavings: number;
306  competitorMonthly: number;
307  competitorNetSavings: number;
308  pourifyDifference: number;
309  language: string;
310}
311
312const ZAReportCapture = (props: ZAReportCaptureProps) => {
313  const { t } = useTranslation();
314  const [email, setEmail] = useState("");
315  const [contactName, setContactName] = useState("");
316  const [phone, setPhone] = useState("");
317  const [venueName, setVenueName] = useState("");
318  const [company, setCompany] = useState("");
319  const [pos, setPos] = useState("");
320  const [pourers, setPourers] = useState("");
321  const [loading, setLoading] = useState(false);
322  const [sent, setSent] = useState(false);
323  const [honeypot, setHoneypot] = useState("");
324
325  const handleSubmit = async (e: React.FormEvent) => {
326    e.preventDefault();
327    if (honeypot || !email || !contactName) return;
328
329    setLoading(true);
330    try {
331      // Save lead
332      await supabase.from("roi_leads" as any).insert({
333        email,
334        contact_name: contactName,
335        phone: phone || null,
336        venue_name: venueName || null,
337        brands: props.brands,
338        net_saving: props.pourifyNetSavings,
339        region: "ZA",
340        loss_percent: props.lossPercent,
341        bottles_per_brand: props.bottlesPerBrand,
342        price_per_bottle: props.pricePerTot,
343        annual_lost_revenue: props.annualLoss,
344        currency_symbol: "R",
345        locale: "en-ZA",
346        language: props.language,
347        monthly_software_cost: props.pourifyMonthly,
348        pourify_annual_cost: props.pourifyMonthly * 12,
349        one_time_investment: ZA_SETUP_FEE,
350        annual_spillage_cost: props.annualLoss,
351      } as any);
352
353      // Send report
354      const { buildLeadContext } = await import("@/lib/sessionContext");
355      await supabase.functions.invoke("send-roi-report", {
356        body: {
357          email,
358          contactName,
359          phone: phone || undefined,
360          venueName: venueName || undefined,
361          company: company || undefined,
362          posSystem: pos || undefined,
363          spiritBrands: \`\${props.brands}\`,
364          brands: props.brands,
365          numberOfBars: 1,
366          bottlesPerBrand: props.bottlesPerBrand,
367          pricePerBottle: props.pricePerTot,
368          lossPercent: props.lossPercent,
369          netSaving: props.pourifyNetSavings,
370          annualSpillageCost: props.annualLoss,
371          annualLostRevenue: props.annualLoss,
372          pourifyAnnualCost: props.pourifyMonthly * 12,
373          oneTimeInvestment: ZA_SETUP_FEE,
374          monthlySoftwareCost: props.pourifyMonthly,
375          region: "ZA",
376          currencySymbol: "R",
377          locale: "en-ZA",
378          language: props.language,
379          competitorMonthly: props.competitorMonthly,
380          competitorNetSavings: props.competitorNetSavings,
381          pourifyDifference: props.pourifyDifference,
382          leadContext: buildLeadContext({ email }),
383        },
384      });
385
386      trackROIReport({
387        email,
388        region: "ZA",
389        brands: props.brands,
390        net_saving: props.pourifyNetSavings,
391      });
392
393      setSent(true);
394      toast.success("ROI report sent! Check your inbox.");
395    } catch {
396      toast.error("Something went wrong. Please try again.");
397    } finally {
398      setLoading(false);
399    }
400  };
401
402  if (sent) {
403    return (
404      <div className="mt-6 rounded-xl bg-background/60 p-5 flex items-center gap-3">
405        <CheckCircle2 className="w-5 h-5 text-accent shrink-0" />
406        <p className="text-sm text-foreground">Your personalised ROI report has been sent. Check your inbox!</p>
407      </div>
408    );
409  }
410
411  return (
412    <div className="mt-6 rounded-xl bg-background/60 p-5">
413      <p className="text-sm font-semibold text-foreground mb-1">📊 Get your ROI report + request a quote</p>
414      <p className="text-xs text-muted-foreground mb-3">Enter your details to receive a detailed savings breakdown and a personalised quote.</p>
415      <form onSubmit={handleSubmit} className="space-y-3">
416        <div className="absolute opacity-0 pointer-events-none" aria-hidden="true" tabIndex={-1}>
417          <input name="website" type="text" autoComplete="off" value={honeypot} onChange={(e) => setHoneypot(e.target.value)} tabIndex={-1} />
418        </div>
419        <div className="grid sm:grid-cols-2 gap-3">
420          <Input
421            type="text"
422            placeholder="Full name *"
423            value={contactName}
424            onChange={(e) => setContactName(e.target.value)}
425            required
426          />
427          <Input
428            type="email"
429            placeholder="Email address *"
430            value={email}
431            onChange={(e) => setEmail(e.target.value)}
432            required
433          />
434        </div>
435        <Input
436          type="text"
437          placeholder="Company / Venue name"
438          value={venueName}
439          onChange={(e) => setVenueName(e.target.value)}
440        />
441        <div className="grid sm:grid-cols-2 gap-3">
442          <Input
443            type="tel"
444            inputMode="tel"
445            placeholder="Cellphone number (e.g. +27 82 123 4567)"
446            value={phone}
447            onChange={(e) => setPhone(e.target.value)}
448            required
449          />
450          <select
451            value={pos}
452            onChange={(e) => setPos(e.target.value)}
453            className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm r
453ing-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
454          >
455            <option value="">POS system</option>
456            <option value="Pilot POS">Pilot POS</option>
457            <option value="SallyPOS">SallyPOS</option>
458            <option value="Lightspeed">Lightspeed</option>
459            <option value="Micros">Micros</option>
460            <option value="Other">Other</option>
461          </select>
462        </div>
463        <Button type="submit" size="sm" disabled={loading} className="shrink-0">
464          {loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Mail className="w-4 h-4 mr-1" />}
465          Send Report & Quote
466        </Button>
467      </form>
468    </div>
469  );
470};
471
472export default ZAROICalculator;
473`;export{n as default};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.