Byggt abonnemangsbeställning
X-Lovable-Edit-ID: edt-e769b5d5-67e6-48c7-b9eb-8798ecfee344 Co-authored-by: wolfoftyreso-debug <250630591+wolfoftyreso-debug@users.noreply.github.com>
This commit is contained in:
@@ -1,93 +0,0 @@
|
|||||||
interface Plan {
|
|
||||||
kg: number;
|
|
||||||
pricePerKg: number;
|
|
||||||
shopifyLink: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AbonnemangCheckoutProps {
|
|
||||||
selectedPlan: string;
|
|
||||||
onBack?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const plans: Record<string, Plan> = {
|
|
||||||
"Liten": {
|
|
||||||
kg: 3,
|
|
||||||
pricePerKg: 325,
|
|
||||||
shopifyLink: "#" // Replace with actual Shopify link
|
|
||||||
},
|
|
||||||
"Mellan": {
|
|
||||||
kg: 5,
|
|
||||||
pricePerKg: 325,
|
|
||||||
shopifyLink: "#"
|
|
||||||
},
|
|
||||||
"Stor": {
|
|
||||||
kg: 10,
|
|
||||||
pricePerKg: 400,
|
|
||||||
shopifyLink: "#"
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const AbonnemangCheckout = ({ selectedPlan, onBack }: AbonnemangCheckoutProps) => {
|
|
||||||
const planName = selectedPlan.split(" (")[0]; // Extract plan name from "Liten (3 kg/vecka)"
|
|
||||||
const plan = plans[planName] || plans["Mellan"];
|
|
||||||
const weeklyPrice = plan.kg * plan.pricePerKg;
|
|
||||||
const monthlyPrice = weeklyPrice * 4;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<div className="w-24 h-px bg-primary/40 mx-auto mb-6"></div>
|
|
||||||
|
|
||||||
<h2 className="font-display text-3xl md:text-4xl font-semibold mb-4">
|
|
||||||
Ditt abonnemang: {planName}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div className="text-muted-foreground mb-6 space-y-2">
|
|
||||||
<p>
|
|
||||||
Du beställer <strong className="text-foreground">{plan.kg} kg</strong> kakor per vecka.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Pris: <strong className="text-foreground">{weeklyPrice.toLocaleString("sv-SE")} kr/vecka</strong>{" "}
|
|
||||||
<span className="text-sm">(~{monthlyPrice.toLocaleString("sv-SE")} kr/månad exkl. moms)</span>
|
|
||||||
</p>
|
|
||||||
<p className="text-primary font-semibold mt-4">
|
|
||||||
Kakorna levereras nästa tisdag!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground text-sm mb-8">
|
|
||||||
Betala direkt via kort eller med Klarna. Ingen manuell kontakt behövs.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row gap-4 justify-center mb-8">
|
|
||||||
<a
|
|
||||||
href={plan.shopifyLink}
|
|
||||||
className="btn-classic px-8 py-4 text-center"
|
|
||||||
>
|
|
||||||
Betala med kort
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href={plan.shopifyLink + "?payment=klarna"}
|
|
||||||
className="btn-outline-classic px-8 py-4 text-center"
|
|
||||||
>
|
|
||||||
Betala med Klarna
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
När betalningen är genomförd skickas automatiskt ett bekräftelsemail
|
|
||||||
och kakorna levereras nästa tisdag.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{onBack && (
|
|
||||||
<button
|
|
||||||
onClick={onBack}
|
|
||||||
className="mt-6 text-primary underline hover:no-underline"
|
|
||||||
>
|
|
||||||
← Tillbaka till formulär
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AbonnemangCheckout;
|
|
||||||
@@ -1,328 +0,0 @@
|
|||||||
import { useState, useEffect } from "react";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
|
||||||
import { supabase } from "@/integrations/supabase/client";
|
|
||||||
|
|
||||||
// Validate Stockholm postal codes (100 00 - 199 99)
|
|
||||||
const isStockholmPostalCode = (postalCode: string) => {
|
|
||||||
const cleaned = postalCode.replace(/\s/g, "");
|
|
||||||
if (!/^\d{5}$/.test(cleaned)) return false;
|
|
||||||
const num = parseInt(cleaned, 10);
|
|
||||||
return num >= 10000 && num <= 19999;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get error message for postal code (for real-time validation display)
|
|
||||||
const getPostalCodeError = (postalCode: string) => {
|
|
||||||
if (!postalCode || postalCode.trim() === "") return null;
|
|
||||||
const cleaned = postalCode.replace(/\s/g, "");
|
|
||||||
if (cleaned.length < 5) return null; // Don't show error while typing
|
|
||||||
if (!/^\d{5}$/.test(cleaned)) return "Ange ett giltigt postnummer (5 siffror)";
|
|
||||||
const num = parseInt(cleaned, 10);
|
|
||||||
if (num < 10000 || num > 19999) {
|
|
||||||
return "Vi levererar endast inom Storstockholm (100 00 – 199 99)";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
|
||||||
företag: z.string().trim().min(1, "Fyll i företagsnamn").max(100),
|
|
||||||
epost: z.string().trim().email("Ange en giltig e-postadress").max(255),
|
|
||||||
telefon: z.string().trim().max(20).optional(),
|
|
||||||
adress: z.string().trim().max(200).optional(),
|
|
||||||
postnummer: z
|
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.min(1, "Fyll i postnummer för leverans")
|
|
||||||
.max(10)
|
|
||||||
.refine(
|
|
||||||
(val) => isStockholmPostalCode(val),
|
|
||||||
{ message: "Vi levererar endast inom Storstockholm (100 00 – 199 99)" }
|
|
||||||
),
|
|
||||||
stad: z.string().trim().max(100).optional(),
|
|
||||||
abonnemang: z.string().min(1, "Välj ett abonnemang"),
|
|
||||||
kommentarer: z.string().trim().max(1000).optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
interface AbonnemangFormProps {
|
|
||||||
selectedPlan?: string;
|
|
||||||
onClose?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AbonnemangForm = ({ selectedPlan, onClose }: AbonnemangFormProps) => {
|
|
||||||
const { user } = useAuth();
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
företag: "",
|
|
||||||
epost: "",
|
|
||||||
telefon: "",
|
|
||||||
adress: "",
|
|
||||||
postnummer: "",
|
|
||||||
stad: "",
|
|
||||||
abonnemang: selectedPlan || "",
|
|
||||||
kommentarer: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const [submitted, setSubmitted] = useState(false);
|
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
||||||
|
|
||||||
// Pre-fill form with user's profile data if logged in
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchProfileData = async () => {
|
|
||||||
if (user) {
|
|
||||||
const { data } = await supabase
|
|
||||||
.from("profiles")
|
|
||||||
.select("display_name, phone, address")
|
|
||||||
.eq("user_id", user.id)
|
|
||||||
.maybeSingle();
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
telefon: data.phone || prev.telefon,
|
|
||||||
epost: user.email || prev.epost,
|
|
||||||
adress: data.address?.split(",")[0]?.trim() || prev.adress,
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
epost: user.email || prev.epost,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchProfileData();
|
|
||||||
}, [user]);
|
|
||||||
|
|
||||||
const handleChange = (
|
|
||||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
|
|
||||||
) => {
|
|
||||||
const { name, value } = e.target;
|
|
||||||
setFormData({ ...formData, [name]: value });
|
|
||||||
// Clear error when user types
|
|
||||||
if (errors[name]) {
|
|
||||||
setErrors({ ...errors, [name]: "" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveAddressToProfile = async () => {
|
|
||||||
if (!user) return;
|
|
||||||
|
|
||||||
// Combine address fields into one string
|
|
||||||
const fullAddress = [
|
|
||||||
formData.adress,
|
|
||||||
formData.postnummer && formData.stad
|
|
||||||
? `${formData.postnummer} ${formData.stad}`
|
|
||||||
: formData.postnummer || formData.stad,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(", ");
|
|
||||||
|
|
||||||
if (fullAddress) {
|
|
||||||
await supabase
|
|
||||||
.from("profiles")
|
|
||||||
.update({
|
|
||||||
address: fullAddress,
|
|
||||||
phone: formData.telefon || null,
|
|
||||||
})
|
|
||||||
.eq("user_id", user.id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setErrors({});
|
|
||||||
|
|
||||||
const result = formSchema.safeParse(formData);
|
|
||||||
if (!result.success) {
|
|
||||||
const fieldErrors: Record<string, string> = {};
|
|
||||||
result.error.errors.forEach((err) => {
|
|
||||||
if (err.path[0]) {
|
|
||||||
fieldErrors[err.path[0] as string] = err.message;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setErrors(fieldErrors);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save address to profile if user is logged in
|
|
||||||
await saveAddressToProfile();
|
|
||||||
|
|
||||||
// For now, just show success - backend integration can be added later
|
|
||||||
setSubmitted(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (submitted) {
|
|
||||||
return (
|
|
||||||
<div className="text-center py-12 px-6">
|
|
||||||
<div className="w-24 h-px bg-primary/40 mx-auto mb-6"></div>
|
|
||||||
<h2 className="font-display text-3xl md:text-4xl font-semibold mb-4">
|
|
||||||
Tack för din beställning!
|
|
||||||
</h2>
|
|
||||||
<p className="text-muted-foreground mb-6">
|
|
||||||
Vi har tagit emot din beställning för{" "}
|
|
||||||
<strong className="text-primary">{formData.abonnemang}</strong> och
|
|
||||||
kontaktar dig inom kort.
|
|
||||||
</p>
|
|
||||||
{onClose && (
|
|
||||||
<button onClick={onClose} className="btn-classic">
|
|
||||||
Stäng
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
|
||||||
<div className="text-center mb-8">
|
|
||||||
<h2 className="font-display text-3xl font-semibold mb-2">
|
|
||||||
Beställ abonnemang
|
|
||||||
</h2>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Fyll i formuläret så kontaktar vi dig
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Företagsnamn */}
|
|
||||||
<div>
|
|
||||||
<label className="block mb-2 font-semibold text-foreground">
|
|
||||||
Företagsnamn <span className="text-primary">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="företag"
|
|
||||||
value={formData.företag}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full px-4 py-3 rounded-sm bg-background border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-primary/30"
|
|
||||||
/>
|
|
||||||
{errors.företag && (
|
|
||||||
<p className="text-destructive text-sm mt-1">{errors.företag}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* E-post & Telefon */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block mb-2 font-semibold text-foreground">
|
|
||||||
E-post <span className="text-primary">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
name="epost"
|
|
||||||
value={formData.epost}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full px-4 py-3 rounded-sm bg-background border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-primary/30"
|
|
||||||
/>
|
|
||||||
{errors.epost && (
|
|
||||||
<p className="text-destructive text-sm mt-1">{errors.epost}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block mb-2 font-semibold text-foreground">
|
|
||||||
Telefon
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="tel"
|
|
||||||
name="telefon"
|
|
||||||
value={formData.telefon}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full px-4 py-3 rounded-sm bg-background border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-primary/30"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Adress */}
|
|
||||||
<div>
|
|
||||||
<label className="block mb-2 font-semibold text-foreground">
|
|
||||||
Leveransadress
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="adress"
|
|
||||||
value={formData.adress}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full px-4 py-3 rounded-sm bg-background border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-primary/30"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Postnummer & Stad */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block mb-2 font-semibold text-foreground">
|
|
||||||
Postnummer <span className="text-primary">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="postnummer"
|
|
||||||
placeholder="t.ex. 114 32"
|
|
||||||
value={formData.postnummer}
|
|
||||||
onChange={handleChange}
|
|
||||||
className={`w-full px-4 py-3 rounded-sm bg-background border text-foreground focus:outline-none focus:ring-2 ${
|
|
||||||
getPostalCodeError(formData.postnummer)
|
|
||||||
? "border-destructive focus:ring-destructive/30"
|
|
||||||
: "border-border focus:ring-primary/30"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
{(errors.postnummer || getPostalCodeError(formData.postnummer)) && (
|
|
||||||
<p className="text-destructive text-sm mt-1">
|
|
||||||
{errors.postnummer || getPostalCodeError(formData.postnummer)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block mb-2 font-semibold text-foreground">
|
|
||||||
Stad
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="stad"
|
|
||||||
value={formData.stad}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full px-4 py-3 rounded-sm bg-background border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-primary/30"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Abonnemang */}
|
|
||||||
<div>
|
|
||||||
<label className="block mb-2 font-semibold text-foreground">
|
|
||||||
Abonnemang <span className="text-primary">*</span>
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
name="abonnemang"
|
|
||||||
value={formData.abonnemang}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full px-4 py-3 rounded-sm bg-background border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-primary/30"
|
|
||||||
>
|
|
||||||
<option value="">Välj abonnemang</option>
|
|
||||||
<option value="Liten (3 kg/vecka)">Liten – 3 kg/vecka – 975 kr</option>
|
|
||||||
<option value="Mellan (5 kg/vecka)">Mellan – 5 kg/vecka – 1 625 kr</option>
|
|
||||||
<option value="Stor (10 kg/vecka)">Stor – 10 kg/vecka – 4 000 kr</option>
|
|
||||||
</select>
|
|
||||||
{errors.abonnemang && (
|
|
||||||
<p className="text-destructive text-sm mt-1">{errors.abonnemang}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Kommentarer */}
|
|
||||||
<div>
|
|
||||||
<label className="block mb-2 font-semibold text-foreground">
|
|
||||||
Kommentarer
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
name="kommentarer"
|
|
||||||
value={formData.kommentarer}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full px-4 py-3 rounded-sm bg-background border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-primary/30"
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Submit */}
|
|
||||||
<button type="submit" className="w-full btn-classic py-4 text-lg">
|
|
||||||
Skicka beställning
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AbonnemangForm;
|
|
||||||
+301
-226
@@ -1,65 +1,50 @@
|
|||||||
import { useState, useMemo } from "react";
|
import { useState, useMemo, useEffect } from "react";
|
||||||
import { Loader2, CreditCard, FileText } from "lucide-react";
|
import { Loader2, CheckCircle2 } from "lucide-react";
|
||||||
import {
|
import { supabase } from "@/integrations/supabase/client";
|
||||||
storefrontApiRequest,
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
CART_CREATE_MUTATION,
|
|
||||||
formatCheckoutUrl,
|
|
||||||
} from "@/lib/shopify";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
const PRICE_PER_KG = 325;
|
const PRICE_PER_KG = 325;
|
||||||
const KG_PER_EMPLOYEE = 0.3;
|
const KG_PER_EMPLOYEE = 0.3;
|
||||||
|
|
||||||
const availablePlans = [
|
const isStockholmPostalCode = (postalCode: string) => {
|
||||||
{ kg: 3, handle: "liten-kakabonnemang-3-kg-vecka", name: "Liten" },
|
const cleaned = postalCode.replace(/\s/g, "");
|
||||||
{ kg: 5, handle: "mellan-kakabonnemang-5-kg-vecka", name: "Mellan" },
|
if (!/^\d{5}$/.test(cleaned)) return false;
|
||||||
{ kg: 10, handle: "stor-kakabonnemang-10-kg-vecka", name: "Stor" },
|
const num = parseInt(cleaned, 10);
|
||||||
];
|
return num >= 10000 && num <= 19999;
|
||||||
|
|
||||||
const pickPlan = (kg: number) => {
|
|
||||||
const match = availablePlans.find((p) => p.kg >= kg);
|
|
||||||
return match || availablePlans[availablePlans.length - 1];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const PRODUCT_BY_HANDLE_QUERY = `
|
const formSchema = z.object({
|
||||||
query ProductByHandle($handle: String!) {
|
företag: z.string().trim().min(1, "Fyll i företagsnamn").max(100),
|
||||||
productByHandle(handle: $handle) {
|
epost: z.string().trim().email("Ange en giltig e-postadress").max(255),
|
||||||
id
|
telefon: z.string().trim().max(30).optional(),
|
||||||
title
|
adress: z.string().trim().max(200).optional(),
|
||||||
handle
|
postnummer: z
|
||||||
variants(first: 1) {
|
.string()
|
||||||
edges {
|
.trim()
|
||||||
node {
|
.min(1, "Fyll i postnummer")
|
||||||
id
|
.refine(isStockholmPostalCode, "Vi levererar endast inom Storstockholm (100 00 – 199 99)"),
|
||||||
availableForSale
|
stad: z.string().trim().max(100).optional(),
|
||||||
}
|
kommentarer: z.string().trim().max(1000).optional(),
|
||||||
}
|
});
|
||||||
}
|
|
||||||
sellingPlanGroups(first: 5) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
name
|
|
||||||
sellingPlans(first: 10) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
type PaymentMethod = "card" | "invoice";
|
|
||||||
|
|
||||||
const PlansSection = () => {
|
const PlansSection = () => {
|
||||||
|
const { user } = useAuth();
|
||||||
const [employees, setEmployees] = useState(10);
|
const [employees, setEmployees] = useState(10);
|
||||||
const [loadingMethod, setLoadingMethod] = useState<PaymentMethod | null>(null);
|
const [showForm, setShowForm] = useState(false);
|
||||||
const [showPaymentOptions, setShowPaymentOptions] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [submitted, setSubmitted] = useState(false);
|
||||||
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
företag: "",
|
||||||
|
epost: "",
|
||||||
|
telefon: "",
|
||||||
|
adress: "",
|
||||||
|
postnummer: "",
|
||||||
|
stad: "",
|
||||||
|
kommentarer: "",
|
||||||
|
});
|
||||||
|
|
||||||
const recommendedKg = useMemo(
|
const recommendedKg = useMemo(
|
||||||
() => Math.max(1, Math.round(employees * KG_PER_EMPLOYEE)),
|
() => Math.max(1, Math.round(employees * KG_PER_EMPLOYEE)),
|
||||||
@@ -67,83 +52,67 @@ const PlansSection = () => {
|
|||||||
);
|
);
|
||||||
const weeklyPrice = recommendedKg * PRICE_PER_KG;
|
const weeklyPrice = recommendedKg * PRICE_PER_KG;
|
||||||
|
|
||||||
const handleStart = async (method: PaymentMethod) => {
|
// Prefyll från profil om inloggad
|
||||||
const plan = pickPlan(recommendedKg);
|
useEffect(() => {
|
||||||
setLoadingMethod(method);
|
if (!user || !showForm) return;
|
||||||
|
(async () => {
|
||||||
|
const { data } = await supabase
|
||||||
|
.from("profiles")
|
||||||
|
.select("company_name, phone, street_address, postal_code, city")
|
||||||
|
.eq("user_id", user.id)
|
||||||
|
.maybeSingle();
|
||||||
|
setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
företag: prev.företag || data?.company_name || "",
|
||||||
|
epost: prev.epost || user.email || "",
|
||||||
|
telefon: prev.telefon || data?.phone || "",
|
||||||
|
adress: prev.adress || data?.street_address || "",
|
||||||
|
postnummer: prev.postnummer || data?.postal_code || "",
|
||||||
|
stad: prev.stad || data?.city || "",
|
||||||
|
}));
|
||||||
|
})();
|
||||||
|
}, [user, showForm]);
|
||||||
|
|
||||||
try {
|
const handleChange = (
|
||||||
const productData = await storefrontApiRequest(PRODUCT_BY_HANDLE_QUERY, {
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||||
handle: plan.handle,
|
) => {
|
||||||
});
|
const { name, value } = e.target;
|
||||||
|
setForm((p) => ({ ...p, [name]: value }));
|
||||||
const product = productData?.data?.productByHandle;
|
if (errors[name]) setErrors((p) => ({ ...p, [name]: "" }));
|
||||||
if (!product) {
|
|
||||||
toast.error(`Kunde inte hitta produkten "${plan.handle}" i Shopify.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const variant = product.variants?.edges?.[0]?.node;
|
|
||||||
if (!variant?.id) {
|
|
||||||
toast.error("Ingen variant tillgänglig på produkten.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sellingPlanId =
|
|
||||||
product.sellingPlanGroups?.edges?.[0]?.node?.sellingPlans?.edges?.[0]?.node?.id;
|
|
||||||
|
|
||||||
if (!sellingPlanId) {
|
|
||||||
toast.error(
|
|
||||||
"Ingen Appstle-abonnemangsplan hittades. Aktivera Storefront-behörigheten 'unauthenticated_read_selling_plans' och koppla en Appstle-plan till produkten.",
|
|
||||||
{ duration: 10000 }
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await storefrontApiRequest(CART_CREATE_MUTATION, {
|
|
||||||
input: {
|
|
||||||
lines: [
|
|
||||||
{
|
|
||||||
quantity: 1,
|
|
||||||
merchandiseId: variant.id,
|
|
||||||
sellingPlanId,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
attributes: [
|
|
||||||
{ key: "Antal anställda", value: String(employees) },
|
|
||||||
{ key: "Rekommenderad mängd", value: `${recommendedKg} kg/vecka` },
|
|
||||||
{ key: "Veckopris (exkl. moms)", value: `${weeklyPrice} kr` },
|
|
||||||
{
|
|
||||||
key: "Önskad betalningsmetod",
|
|
||||||
value: method === "card" ? "Automatiskt kort varje månad" : "Månadsfaktura",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const errors = data?.data?.cartCreate?.userErrors || [];
|
|
||||||
if (errors.length > 0) {
|
|
||||||
console.error("Cart creation failed:", errors);
|
|
||||||
toast.error("Kunde inte skapa checkout: " + errors[0].message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkoutUrl = data?.data?.cartCreate?.cart?.checkoutUrl;
|
|
||||||
if (!checkoutUrl) {
|
|
||||||
toast.error("Ingen checkout-länk mottagen");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.open(formatCheckoutUrl(checkoutUrl), "_blank");
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
toast.error("Något gick fel vid checkout");
|
|
||||||
} finally {
|
|
||||||
setLoadingMethod(null);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const result = formSchema.safeParse(form);
|
||||||
|
if (!result.success) {
|
||||||
|
const fe: Record<string, string> = {};
|
||||||
|
result.error.errors.forEach((err) => {
|
||||||
|
if (err.path[0]) fe[err.path[0] as string] = err.message;
|
||||||
|
});
|
||||||
|
setErrors(fe);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const isBusy = loadingMethod !== null;
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase.functions.invoke("submit-order", {
|
||||||
|
body: {
|
||||||
|
...result.data,
|
||||||
|
employees,
|
||||||
|
kg_per_vecka: recommendedKg,
|
||||||
|
pris_per_vecka: weeklyPrice,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (error) throw error;
|
||||||
|
if ((data as any)?.error) throw new Error((data as any).error);
|
||||||
|
setSubmitted(true);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
toast.error("Kunde inte skicka beställningen. Försök igen.");
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="abonnemang" className="py-16 md:py-28 px-6 bg-background">
|
<section id="abonnemang" className="py-16 md:py-28 px-6 bg-background">
|
||||||
@@ -161,114 +130,220 @@ const PlansSection = () => {
|
|||||||
|
|
||||||
{/* Calculator card */}
|
{/* Calculator card */}
|
||||||
<div className="bg-card border border-border rounded-sm shadow-[0_10px_40px_-15px_rgba(0,0,0,0.15)] p-6 md:p-12">
|
<div className="bg-card border border-border rounded-sm shadow-[0_10px_40px_-15px_rgba(0,0,0,0.15)] p-6 md:p-12">
|
||||||
{/* Employee count display */}
|
{submitted ? (
|
||||||
<div className="text-center mb-8">
|
<div className="text-center py-8 animate-fade-in">
|
||||||
<div
|
<CheckCircle2 className="w-16 h-16 text-primary mx-auto mb-4" />
|
||||||
key={employees}
|
<h3 className="font-display text-3xl md:text-4xl font-semibold mb-3">
|
||||||
className="font-display text-6xl md:text-8xl font-semibold text-primary tabular-nums animate-fade-in"
|
Tack för din beställning!
|
||||||
>
|
</h3>
|
||||||
{employees}
|
<p className="text-muted-foreground max-w-md mx-auto">
|
||||||
|
Vi har tagit emot din förfrågan för{" "}
|
||||||
|
<strong className="text-foreground">
|
||||||
|
{recommendedKg} kg kakor per vecka
|
||||||
|
</strong>{" "}
|
||||||
|
och kontaktar dig inom kort för att bekräfta leverans.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground text-sm md:text-base mt-2 tracking-wide uppercase">
|
) : !showForm ? (
|
||||||
{employees === 1 ? "anställd" : "anställda"}
|
<>
|
||||||
</div>
|
{/* Employee count */}
|
||||||
</div>
|
<div className="text-center mb-8">
|
||||||
|
<div
|
||||||
{/* Slider */}
|
key={employees}
|
||||||
<div className="mb-10 md:mb-12">
|
className="font-display text-6xl md:text-8xl font-semibold text-primary tabular-nums animate-fade-in"
|
||||||
<input
|
>
|
||||||
type="range"
|
{employees}
|
||||||
min={1}
|
|
||||||
max={100}
|
|
||||||
step={1}
|
|
||||||
value={employees}
|
|
||||||
onChange={(e) => setEmployees(parseInt(e.target.value, 10))}
|
|
||||||
className="calculator-slider w-full"
|
|
||||||
aria-label="Antal anställda"
|
|
||||||
/>
|
|
||||||
<div className="flex justify-between text-xs text-muted-foreground mt-3">
|
|
||||||
<span>1</span>
|
|
||||||
<span>100</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Result */}
|
|
||||||
<div className="text-center border-t border-border pt-8 md:pt-10 space-y-6">
|
|
||||||
<div>
|
|
||||||
<div className="text-muted-foreground text-xs md:text-sm uppercase tracking-widest mb-2">
|
|
||||||
Rekommenderad mängd
|
|
||||||
</div>
|
|
||||||
<div className="font-display text-4xl md:text-5xl font-semibold text-foreground tabular-nums">
|
|
||||||
{recommendedKg} kg
|
|
||||||
</div>
|
|
||||||
<div className="text-muted-foreground text-sm mt-1">per vecka</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="text-muted-foreground text-xs md:text-sm uppercase tracking-widest mb-2">
|
|
||||||
Pris per vecka
|
|
||||||
</div>
|
|
||||||
<div className="font-display text-3xl md:text-4xl font-semibold text-primary tabular-nums">
|
|
||||||
{weeklyPrice.toLocaleString("sv-SE")} kr
|
|
||||||
</div>
|
|
||||||
<div className="text-muted-foreground text-xs mt-1">exklusive moms</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!showPaymentOptions ? (
|
|
||||||
<button
|
|
||||||
onClick={() => setShowPaymentOptions(true)}
|
|
||||||
disabled={isBusy}
|
|
||||||
className="btn-classic w-full py-4 text-base md:text-lg rounded-sm disabled:opacity-50 mt-2"
|
|
||||||
>
|
|
||||||
Starta abonnemang
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<div className="animate-fade-in space-y-3 pt-2">
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
Välj betalningsmetod
|
|
||||||
</p>
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
||||||
<button
|
|
||||||
onClick={() => handleStart("card")}
|
|
||||||
disabled={isBusy}
|
|
||||||
className="btn-classic py-4 px-4 text-base rounded-sm disabled:opacity-50 flex items-center justify-center gap-2"
|
|
||||||
>
|
|
||||||
{loadingMethod === "card" ? (
|
|
||||||
<Loader2 className="w-5 h-5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<CreditCard className="w-5 h-5" />
|
|
||||||
<span>Automatiskt varje månad</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleStart("invoice")}
|
|
||||||
disabled={isBusy}
|
|
||||||
className="py-4 px-4 text-base rounded-sm border-2 border-primary text-primary hover:bg-primary hover:text-primary-foreground transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
|
|
||||||
>
|
|
||||||
{loadingMethod === "invoice" ? (
|
|
||||||
<Loader2 className="w-5 h-5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<FileText className="w-5 h-5" />
|
|
||||||
<span>Månadsfaktura</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-muted-foreground text-sm md:text-base mt-2 tracking-wide uppercase">
|
||||||
|
{employees === 1 ? "anställd" : "anställda"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Slider */}
|
||||||
|
<div className="mb-10 md:mb-12">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={1}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
value={employees}
|
||||||
|
onChange={(e) => setEmployees(parseInt(e.target.value, 10))}
|
||||||
|
className="calculator-slider w-full"
|
||||||
|
aria-label="Antal anställda"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between text-xs text-muted-foreground mt-3">
|
||||||
|
<span>1</span>
|
||||||
|
<span>100</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Result */}
|
||||||
|
<div className="text-center border-t border-border pt-8 md:pt-10 space-y-6">
|
||||||
|
<div>
|
||||||
|
<div className="text-muted-foreground text-xs md:text-sm uppercase tracking-widest mb-2">
|
||||||
|
Rekommenderad mängd
|
||||||
|
</div>
|
||||||
|
<div className="font-display text-4xl md:text-5xl font-semibold text-foreground tabular-nums">
|
||||||
|
{recommendedKg} kg
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground text-sm mt-1">per vecka</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="text-muted-foreground text-xs md:text-sm uppercase tracking-widest mb-2">
|
||||||
|
Pris per vecka
|
||||||
|
</div>
|
||||||
|
<div className="font-display text-3xl md:text-4xl font-semibold text-primary tabular-nums">
|
||||||
|
{weeklyPrice.toLocaleString("sv-SE")} kr
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground text-xs mt-1">exklusive moms</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowPaymentOptions(false)}
|
onClick={() => setShowForm(true)}
|
||||||
className="text-muted-foreground text-xs underline hover:text-foreground"
|
className="btn-classic w-full py-4 text-base md:text-lg rounded-sm mt-2"
|
||||||
|
>
|
||||||
|
Starta abonnemang
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-5 animate-fade-in">
|
||||||
|
<div className="text-center mb-2">
|
||||||
|
<div className="text-muted-foreground text-xs uppercase tracking-widest mb-1">
|
||||||
|
Din beställning
|
||||||
|
</div>
|
||||||
|
<div className="font-display text-2xl md:text-3xl font-semibold">
|
||||||
|
{recommendedKg} kg/vecka —{" "}
|
||||||
|
<span className="text-primary">
|
||||||
|
{weeklyPrice.toLocaleString("sv-SE")} kr
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground text-xs">
|
||||||
|
exklusive moms · {employees} {employees === 1 ? "anställd" : "anställda"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block mb-1.5 text-sm font-semibold">
|
||||||
|
Företagsnamn <span className="text-primary">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="företag"
|
||||||
|
value={form.företag}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-3 rounded-sm bg-background border border-border focus:outline-none focus:ring-2 focus:ring-primary/30"
|
||||||
|
/>
|
||||||
|
{errors.företag && (
|
||||||
|
<p className="text-destructive text-xs mt-1">{errors.företag}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block mb-1.5 text-sm font-semibold">
|
||||||
|
E-post <span className="text-primary">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="epost"
|
||||||
|
value={form.epost}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-3 rounded-sm bg-background border border-border focus:outline-none focus:ring-2 focus:ring-primary/30"
|
||||||
|
/>
|
||||||
|
{errors.epost && (
|
||||||
|
<p className="text-destructive text-xs mt-1">{errors.epost}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block mb-1.5 text-sm font-semibold">Telefon</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
name="telefon"
|
||||||
|
value={form.telefon}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-3 rounded-sm bg-background border border-border focus:outline-none focus:ring-2 focus:ring-primary/30"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block mb-1.5 text-sm font-semibold">Leveransadress</label>
|
||||||
|
<input
|
||||||
|
name="adress"
|
||||||
|
value={form.adress}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-3 rounded-sm bg-background border border-border focus:outline-none focus:ring-2 focus:ring-primary/30"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block mb-1.5 text-sm font-semibold">
|
||||||
|
Postnummer <span className="text-primary">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="postnummer"
|
||||||
|
placeholder="t.ex. 114 32"
|
||||||
|
value={form.postnummer}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-3 rounded-sm bg-background border border-border focus:outline-none focus:ring-2 focus:ring-primary/30"
|
||||||
|
/>
|
||||||
|
{errors.postnummer && (
|
||||||
|
<p className="text-destructive text-xs mt-1">{errors.postnummer}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block mb-1.5 text-sm font-semibold">Stad</label>
|
||||||
|
<input
|
||||||
|
name="stad"
|
||||||
|
value={form.stad}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-4 py-3 rounded-sm bg-background border border-border focus:outline-none focus:ring-2 focus:ring-primary/30"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block mb-1.5 text-sm font-semibold">Kommentarer</label>
|
||||||
|
<textarea
|
||||||
|
name="kommentarer"
|
||||||
|
value={form.kommentarer}
|
||||||
|
onChange={handleChange}
|
||||||
|
rows={3}
|
||||||
|
className="w-full px-4 py-3 rounded-sm bg-background border border-border focus:outline-none focus:ring-2 focus:ring-primary/30"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowForm(false)}
|
||||||
|
disabled={submitting}
|
||||||
|
className="sm:w-1/3 py-3 rounded-sm border border-border hover:bg-muted text-sm"
|
||||||
>
|
>
|
||||||
← Tillbaka
|
← Tillbaka
|
||||||
</button>
|
</button>
|
||||||
<p className="text-muted-foreground text-xs">
|
<button
|
||||||
Du slutför beställningen säkert hos Shopify.
|
type="submit"
|
||||||
</p>
|
disabled={submitting}
|
||||||
|
className="btn-classic flex-1 py-3 rounded-sm disabled:opacity-50 flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
{submitting ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
Skickar...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Skicka beställning"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<p className="text-center text-muted-foreground text-xs">
|
||||||
</div>
|
Vi kontaktar dig för att bekräfta leverans och betalning.
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { storefrontApiRequest, STOREFRONT_PRODUCTS_QUERY, ShopifyProduct } from '@/lib/shopify';
|
|
||||||
|
|
||||||
export function useShopifyProducts(query?: string) {
|
|
||||||
const [products, setProducts] = useState<ShopifyProduct[]>([]);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchProducts() {
|
|
||||||
setIsLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const data = await storefrontApiRequest(STOREFRONT_PRODUCTS_QUERY, {
|
|
||||||
first: 10,
|
|
||||||
query: query || null
|
|
||||||
});
|
|
||||||
if (data?.data?.products?.edges) {
|
|
||||||
setProducts(data.data.products.edges);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to fetch products:', err);
|
|
||||||
setError('Kunde inte ladda produkter');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchProducts();
|
|
||||||
}, [query]);
|
|
||||||
|
|
||||||
return { products, isLoading, error };
|
|
||||||
}
|
|
||||||
+22
-26
@@ -9,7 +9,7 @@ import { useToast } from "@/hooks/use-toast";
|
|||||||
import { supabase } from "@/integrations/supabase/client";
|
import { supabase } from "@/integrations/supabase/client";
|
||||||
import Header from "@/components/Header";
|
import Header from "@/components/Header";
|
||||||
import { Save, Package } from "lucide-react";
|
import { Save, Package } from "lucide-react";
|
||||||
import { useShopifyProducts } from "@/hooks/useShopifyProducts";
|
|
||||||
|
|
||||||
// Validate Stockholm postal codes (100 00 - 199 99)
|
// Validate Stockholm postal codes (100 00 - 199 99)
|
||||||
const isStockholmPostalCode = (postalCode: string) => {
|
const isStockholmPostalCode = (postalCode: string) => {
|
||||||
@@ -71,7 +71,7 @@ const Account = () => {
|
|||||||
const { user, loading: authLoading, signOut } = useAuth();
|
const { user, loading: authLoading, signOut } = useAuth();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { products, isLoading: productsLoading } = useShopifyProducts("product_type:Abonnemang");
|
|
||||||
|
|
||||||
const hasChanges =
|
const hasChanges =
|
||||||
profile.company_name !== originalProfile.company_name ||
|
profile.company_name !== originalProfile.company_name ||
|
||||||
@@ -302,30 +302,26 @@ const Account = () => {
|
|||||||
<h2 className="font-serif text-xl">Mitt abonnemang</h2>
|
<h2 className="font-serif text-xl">Mitt abonnemang</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{productsLoading ? (
|
<div className="space-y-4">
|
||||||
<p className="text-muted-foreground text-sm">Laddar abonnemang...</p>
|
<p className="text-muted-foreground text-sm">
|
||||||
) : (
|
Du har för närvarande inget aktivt abonnemang.
|
||||||
<div className="space-y-4">
|
</p>
|
||||||
<p className="text-muted-foreground text-sm">
|
<Button
|
||||||
Du har för närvarande inget aktivt abonnemang.
|
variant="outline"
|
||||||
</p>
|
onClick={() => {
|
||||||
<Button
|
navigate("/");
|
||||||
variant="outline"
|
setTimeout(() => {
|
||||||
onClick={() => {
|
const element = document.getElementById("abonnemang");
|
||||||
navigate("/");
|
if (element) {
|
||||||
setTimeout(() => {
|
element.scrollIntoView({ behavior: "smooth" });
|
||||||
const element = document.getElementById("abonnemang");
|
}
|
||||||
if (element) {
|
}, 100);
|
||||||
element.scrollIntoView({ behavior: "smooth" });
|
}}
|
||||||
}
|
className="w-full"
|
||||||
}, 100);
|
>
|
||||||
}}
|
Beställ abonnemang
|
||||||
className="w-full"
|
</Button>
|
||||||
>
|
</div>
|
||||||
Beställ abonnemang
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center mt-6">
|
<div className="text-center mt-6">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -8,3 +8,6 @@ verify_jwt = false
|
|||||||
|
|
||||||
[functions.send-contact-email]
|
[functions.send-contact-email]
|
||||||
verify_jwt = false
|
verify_jwt = false
|
||||||
|
|
||||||
|
[functions.submit-order]
|
||||||
|
verify_jwt = false
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.0";
|
||||||
|
import { corsHeaders } from "npm:@supabase/supabase-js@2/cors";
|
||||||
|
import { z } from "https://esm.sh/zod@3.23.8";
|
||||||
|
|
||||||
|
const BodySchema = z.object({
|
||||||
|
företag: z.string().trim().min(1).max(100),
|
||||||
|
epost: z.string().trim().email().max(255),
|
||||||
|
telefon: z.string().trim().max(30).optional().default(""),
|
||||||
|
adress: z.string().trim().max(200).optional().default(""),
|
||||||
|
postnummer: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.regex(/^\s*\d{3}\s*\d{2}\s*$/, "Ogiltigt postnummer")
|
||||||
|
.refine((v) => {
|
||||||
|
const n = parseInt(v.replace(/\s/g, ""), 10);
|
||||||
|
return n >= 10000 && n <= 19999;
|
||||||
|
}, "Vi levererar endast inom Storstockholm (100 00 – 199 99)"),
|
||||||
|
stad: z.string().trim().max(100).optional().default(""),
|
||||||
|
kommentarer: z.string().trim().max(1000).optional().default(""),
|
||||||
|
employees: z.number().int().min(1).max(1000),
|
||||||
|
kg_per_vecka: z.number().min(1).max(500),
|
||||||
|
pris_per_vecka: z.number().min(0).max(1000000),
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.serve(async (req) => {
|
||||||
|
if (req.method === "OPTIONS") {
|
||||||
|
return new Response("ok", { headers: corsHeaders });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = BodySchema.safeParse(await req.json());
|
||||||
|
if (!parsed.success) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: parsed.error.flatten().fieldErrors }),
|
||||||
|
{
|
||||||
|
status: 400,
|
||||||
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const d = parsed.data;
|
||||||
|
|
||||||
|
const supabase = createClient(
|
||||||
|
Deno.env.get("SUPABASE_URL")!,
|
||||||
|
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
|
||||||
|
);
|
||||||
|
|
||||||
|
const orderNumber = `ABO-${Date.now().toString(36).toUpperCase()}`;
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("orders")
|
||||||
|
.insert({
|
||||||
|
order_number: orderNumber,
|
||||||
|
customer_email: d.epost,
|
||||||
|
customer_name: d.företag,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
typ: "Kakabonnemang",
|
||||||
|
antal_anställda: d.employees,
|
||||||
|
kg_per_vecka: d.kg_per_vecka,
|
||||||
|
pris_per_vecka_exkl_moms: d.pris_per_vecka,
|
||||||
|
valuta: "SEK",
|
||||||
|
leverans: {
|
||||||
|
adress: d.adress,
|
||||||
|
postnummer: d.postnummer,
|
||||||
|
stad: d.stad,
|
||||||
|
},
|
||||||
|
telefon: d.telefon,
|
||||||
|
kommentarer: d.kommentarer,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
total_amount: d.pris_per_vecka,
|
||||||
|
currency: "SEK",
|
||||||
|
status: "pending",
|
||||||
|
})
|
||||||
|
.select("id, order_number")
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("Order insert failed:", error);
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: "Kunde inte spara beställning", details: error.message }),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ ok: true, order: data }), {
|
||||||
|
status: 200,
|
||||||
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("submit-order error:", e);
|
||||||
|
return new Response(JSON.stringify({ error: (e as Error).message }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user