|
@@ -0,0 +1,388 @@
|
|
|
|
|
+import { SidebarProvider } from "@/components/ui/sidebar";
|
|
|
|
|
+import { AppSidebar } from "@/components/AppSidebar";
|
|
|
|
|
+import { Card, CardContent } from "@/components/ui/card";
|
|
|
|
|
+import { Button } from "@/components/ui/button";
|
|
|
|
|
+import { Switch } from "@/components/ui/switch";
|
|
|
|
|
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
|
|
|
+import { useTranslation } from 'react-i18next';
|
|
|
|
|
+import { useShop } from "@/components/context/ShopContext";
|
|
|
|
|
+import { useSubscription, useUpdateSubscription } from "@/hooks/useSubscription";
|
|
|
|
|
+import { usePackages } from "@/hooks/usePlans";
|
|
|
|
|
+import {
|
|
|
|
|
+ Clock,
|
|
|
|
|
+ AlertCircle,
|
|
|
|
|
+ Loader2,
|
|
|
|
|
+ Info,
|
|
|
|
|
+ Package,
|
|
|
|
|
+ Zap,
|
|
|
|
|
+ AlertTriangle
|
|
|
|
|
+} from "lucide-react";
|
|
|
|
|
+import {
|
|
|
|
|
+ getPackagePrice,
|
|
|
|
|
+ formatCurrency,
|
|
|
|
|
+ getUsageProgressColor,
|
|
|
|
|
+ type Currency,
|
|
|
|
|
+ type PackageWithTranslation
|
|
|
|
|
+} from "@/types/subscription.types";
|
|
|
|
|
+import { useNavigate } from "react-router-dom";
|
|
|
|
|
+
|
|
|
|
|
+export default function BillingTopups() {
|
|
|
|
|
+ const { t } = useTranslation();
|
|
|
|
|
+ const { selectedShop } = useShop();
|
|
|
|
|
+ const navigate = useNavigate();
|
|
|
|
|
+
|
|
|
|
|
+ // Hooks
|
|
|
|
|
+ const {
|
|
|
|
|
+ subscription,
|
|
|
|
|
+ isLoading: subLoading,
|
|
|
|
|
+ minutesRemaining,
|
|
|
|
|
+ packageMinutesRemaining,
|
|
|
|
|
+ totalMinutesRemaining,
|
|
|
|
|
+ usagePercentage,
|
|
|
|
|
+ isExpired,
|
|
|
|
|
+ daysRemaining,
|
|
|
|
|
+ refetch: refetchSubscription
|
|
|
|
|
+ } = useSubscription(selectedShop?.id);
|
|
|
|
|
+
|
|
|
|
|
+ const { packages, purchasePackage, isLoading: packagesLoading } = usePackages();
|
|
|
|
|
+ const { updateAutoPurchase, isUpdating } = useUpdateSubscription();
|
|
|
|
|
+
|
|
|
|
|
+ const currentCurrency: Currency = (subscription?.currency as Currency) || 'HUF';
|
|
|
|
|
+ const isLoading = subLoading || packagesLoading;
|
|
|
|
|
+
|
|
|
|
|
+ // Handle package purchase
|
|
|
|
|
+ const handlePurchasePackage = async (pkg: PackageWithTranslation) => {
|
|
|
|
|
+ if (!selectedShop?.id || !subscription?.id) return;
|
|
|
|
|
+
|
|
|
|
|
+ const success = await purchasePackage(
|
|
|
|
|
+ selectedShop.id,
|
|
|
|
|
+ subscription.id,
|
|
|
|
|
+ pkg.id,
|
|
|
|
|
+ pkg,
|
|
|
|
|
+ currentCurrency
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if (success) {
|
|
|
|
|
+ await refetchSubscription();
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // Handle auto-purchase toggle
|
|
|
|
|
+ const handleAutoPurchaseToggle = async (enabled: boolean) => {
|
|
|
|
|
+ if (!selectedShop?.id) return;
|
|
|
|
|
+
|
|
|
|
|
+ await updateAutoPurchase(
|
|
|
|
|
+ selectedShop.id,
|
|
|
|
|
+ enabled,
|
|
|
|
|
+ subscription?.auto_purchase_package_id || packages[0]?.id,
|
|
|
|
|
+ subscription?.auto_purchase_threshold || 5
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ await refetchSubscription();
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // Render loading state
|
|
|
|
|
+ if (isLoading) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <SidebarProvider>
|
|
|
|
|
+ <div className="min-h-screen flex w-full bg-slate-900">
|
|
|
|
|
+ <AppSidebar />
|
|
|
|
|
+ <main className="flex-1 bg-slate-900 text-white flex items-center justify-center">
|
|
|
|
|
+ <Loader2 className="w-8 h-8 animate-spin text-cyan-500" />
|
|
|
|
|
+ </main>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </SidebarProvider>
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // No shop selected
|
|
|
|
|
+ if (!selectedShop) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <SidebarProvider>
|
|
|
|
|
+ <div className="min-h-screen flex w-full bg-slate-900">
|
|
|
|
|
+ <AppSidebar />
|
|
|
|
|
+ <main className="flex-1 bg-slate-900 text-white p-6">
|
|
|
|
|
+ <div className="text-center py-20">
|
|
|
|
|
+ <AlertCircle className="w-16 h-16 text-yellow-500 mx-auto mb-4" />
|
|
|
|
|
+ <h2 className="text-2xl font-bold mb-2">{t('billing.noShopSelected')}</h2>
|
|
|
|
|
+ <p className="text-slate-400">{t('billing.selectShopFirst')}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </main>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </SidebarProvider>
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Show upgrade prompt for trial users
|
|
|
|
|
+ if (subscription?.status === 'trial') {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <SidebarProvider>
|
|
|
|
|
+ <div className="min-h-screen flex w-full bg-slate-900">
|
|
|
|
|
+ <AppSidebar />
|
|
|
|
|
+ <main className="flex-1 bg-slate-900 text-white p-6">
|
|
|
|
|
+ <div className="flex flex-col gap-6">
|
|
|
|
|
+ {/* Header */}
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <h1 className="text-3xl font-bold text-white">{t('billing.topups')}</h1>
|
|
|
|
|
+ <p className="text-slate-400 mt-2">
|
|
|
|
|
+ {t('billing.topupsSubtitle')}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Upgrade Required */}
|
|
|
|
|
+ <Card className="border border-yellow-500/50 bg-yellow-500/10">
|
|
|
|
|
+ <CardContent className="p-8 text-center">
|
|
|
|
|
+ <AlertTriangle className="w-16 h-16 text-yellow-500 mx-auto mb-4" />
|
|
|
|
|
+ <h2 className="text-2xl font-bold text-white mb-2">
|
|
|
|
|
+ {t('billing.upgradeRequired')}
|
|
|
|
|
+ </h2>
|
|
|
|
|
+ <p className="text-slate-400 mb-6 max-w-md mx-auto">
|
|
|
|
|
+ {t('billing.upgradeRequiredDesc')}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ className="bg-cyan-500 hover:bg-cyan-600 text-white"
|
|
|
|
|
+ onClick={() => navigate('/billing/plans')}
|
|
|
|
|
+ >
|
|
|
|
|
+ {t('billing.viewPlans')}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </main>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </SidebarProvider>
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <SidebarProvider>
|
|
|
|
|
+ <div className="min-h-screen flex w-full bg-slate-900">
|
|
|
|
|
+ <AppSidebar />
|
|
|
|
|
+ <main className="flex-1 bg-slate-900 text-white">
|
|
|
|
|
+ <div className="flex flex-col gap-6 p-6">
|
|
|
|
|
+ {/* Header */}
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <h1 className="text-3xl font-bold text-white">{t('billing.topups')}</h1>
|
|
|
|
|
+ <p className="text-slate-400 mt-2">
|
|
|
|
|
+ {t('billing.topupsSubtitle')}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Current Usage Summary */}
|
|
|
|
|
+ <Card className="border border-slate-700 bg-slate-800/50">
|
|
|
|
|
+ <CardContent className="p-6">
|
|
|
|
|
+ <div className="flex items-center justify-between mb-4">
|
|
|
|
|
+ <h3 className="font-semibold text-white flex items-center gap-2">
|
|
|
|
|
+ <Clock className="w-5 h-5 text-cyan-400" />
|
|
|
|
|
+ {t('billing.currentUsage')}
|
|
|
|
|
+ </h3>
|
|
|
|
|
+ <span className="text-sm text-slate-400">
|
|
|
|
|
+ {subscription?.plan_translation?.name || subscription?.plan?.plan_code}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
|
|
|
|
+ <div className="p-4 bg-slate-700/50 rounded-lg">
|
|
|
|
|
+ <div className="text-sm text-slate-400 mb-1">{t('billing.includedMinutes')}</div>
|
|
|
|
|
+ <div className="text-2xl font-bold text-white">
|
|
|
|
|
+ {Math.round(minutesRemaining)} / {subscription?.minutes_included}
|
|
|
|
|
+ <span className="text-sm font-normal text-slate-400 ml-1">{t('sidebar.min')}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="p-4 bg-slate-700/50 rounded-lg">
|
|
|
|
|
+ <div className="text-sm text-slate-400 mb-1">{t('billing.packageMinutes')}</div>
|
|
|
|
|
+ <div className="text-2xl font-bold text-cyan-400">
|
|
|
|
|
+ {Math.round(packageMinutesRemaining)}
|
|
|
|
|
+ <span className="text-sm font-normal text-slate-400 ml-1">{t('sidebar.min')}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="p-4 bg-slate-700/50 rounded-lg">
|
|
|
|
|
+ <div className="text-sm text-slate-400 mb-1">{t('billing.totalRemaining')}</div>
|
|
|
|
|
+ <div className={`text-2xl font-bold ${
|
|
|
|
|
+ totalMinutesRemaining <= 5 ? 'text-red-400' :
|
|
|
|
|
+ totalMinutesRemaining <= 15 ? 'text-yellow-400' :
|
|
|
|
|
+ 'text-green-400'
|
|
|
|
|
+ }`}>
|
|
|
|
|
+ {Math.round(totalMinutesRemaining)}
|
|
|
|
|
+ <span className="text-sm font-normal text-slate-400 ml-1">{t('sidebar.min')}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Progress bar */}
|
|
|
|
|
+ <div className="h-2 bg-slate-700 rounded-full overflow-hidden">
|
|
|
|
|
+ <div
|
|
|
|
|
+ className={`h-full transition-all duration-300 ${getUsageProgressColor(usagePercentage)}`}
|
|
|
|
|
+ style={{ width: `${Math.min(usagePercentage, 100)}%` }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex justify-between text-xs text-slate-500 mt-1">
|
|
|
|
|
+ <span>{Math.round(subscription?.minutes_used || 0)} {t('billing.used')}</span>
|
|
|
|
|
+ <span>{subscription?.minutes_included} {t('billing.included')}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Additional Packages Section */}
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <h2 className="text-xl font-semibold text-white mb-4 flex items-center gap-2">
|
|
|
|
|
+ <Package className="w-5 h-5 text-cyan-400" />
|
|
|
|
|
+ {t('billing.additionalPackages')}
|
|
|
|
|
+ </h2>
|
|
|
|
|
+ <p className="text-slate-400 mb-6">
|
|
|
|
|
+ {t('billing.additionalPackagesDescription')}
|
|
|
|
|
+ </p>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
|
|
|
+ {packages.map(pkg => (
|
|
|
|
|
+ <Card
|
|
|
|
|
+ key={pkg.id}
|
|
|
|
|
+ className="border border-slate-700 bg-slate-800/50 hover:border-cyan-500/50 transition-all"
|
|
|
|
|
+ >
|
|
|
|
|
+ <CardContent className="p-6">
|
|
|
|
|
+ <div className="flex items-center gap-3 mb-4">
|
|
|
|
|
+ <div className="w-12 h-12 rounded-lg bg-cyan-500/20 flex items-center justify-center">
|
|
|
|
|
+ <Zap className="w-6 h-6 text-cyan-400" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <span className="font-semibold text-white text-lg">
|
|
|
|
|
+ {pkg.translations.name}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <div className="text-sm text-slate-400">
|
|
|
|
|
+ {pkg.minutes} {t('sidebar.min')}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <p className="text-sm text-slate-400 mb-4">
|
|
|
|
|
+ {pkg.translations.description}
|
|
|
|
|
+ </p>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="flex items-center justify-between mb-4">
|
|
|
|
|
+ <span className="text-2xl font-bold text-white">
|
|
|
|
|
+ {formatCurrency(getPackagePrice(pkg, currentCurrency), currentCurrency)}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span className="text-xs text-slate-500">+ {t('billing.vat')}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <Button
|
|
|
|
|
+ className="w-full bg-cyan-500 hover:bg-cyan-600 text-white"
|
|
|
|
|
+ onClick={() => handlePurchasePackage(pkg)}
|
|
|
|
|
+ >
|
|
|
|
|
+ {t('billing.purchasePackage')}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Auto-Purchase Settings */}
|
|
|
|
|
+ <Card className="border border-slate-700 bg-slate-800/50">
|
|
|
|
|
+ <CardContent className="p-6">
|
|
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <h3 className="font-semibold text-white flex items-center gap-2">
|
|
|
|
|
+ <Zap className="w-5 h-5 text-yellow-400" />
|
|
|
|
|
+ {t('billing.autoPurchase')}
|
|
|
|
|
+ </h3>
|
|
|
|
|
+ <p className="text-sm text-slate-400 mt-1">
|
|
|
|
|
+ {t('billing.autoPurchaseDescription')}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Switch
|
|
|
|
|
+ checked={subscription?.auto_purchase_enabled || false}
|
|
|
|
|
+ onCheckedChange={handleAutoPurchaseToggle}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {subscription?.auto_purchase_enabled && (
|
|
|
|
|
+ <div className="mt-4 pt-4 border-t border-slate-700 grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <label className="text-sm text-slate-400 mb-2 block">
|
|
|
|
|
+ {t('billing.autoPurchasePackage')}
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <Select
|
|
|
|
|
+ value={subscription.auto_purchase_package_id || ''}
|
|
|
|
|
+ onValueChange={(value) => {
|
|
|
|
|
+ if (selectedShop?.id) {
|
|
|
|
|
+ updateAutoPurchase(
|
|
|
|
|
+ selectedShop.id,
|
|
|
|
|
+ true,
|
|
|
|
|
+ value,
|
|
|
|
|
+ subscription.auto_purchase_threshold
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <SelectTrigger className="bg-slate-700 border-slate-600">
|
|
|
|
|
+ <SelectValue />
|
|
|
|
|
+ </SelectTrigger>
|
|
|
|
|
+ <SelectContent className="bg-slate-800 border-slate-600">
|
|
|
|
|
+ {packages.map(pkg => (
|
|
|
|
|
+ <SelectItem key={pkg.id} value={pkg.id} className="text-white">
|
|
|
|
|
+ {pkg.translations.name} - {formatCurrency(getPackagePrice(pkg, currentCurrency), currentCurrency)}
|
|
|
|
|
+ </SelectItem>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </SelectContent>
|
|
|
|
|
+ </Select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <label className="text-sm text-slate-400 mb-2 block">
|
|
|
|
|
+ {t('billing.autoPurchaseThreshold')}
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <Select
|
|
|
|
|
+ value={String(subscription.auto_purchase_threshold)}
|
|
|
|
|
+ onValueChange={(value) => {
|
|
|
|
|
+ if (selectedShop?.id) {
|
|
|
|
|
+ updateAutoPurchase(
|
|
|
|
|
+ selectedShop.id,
|
|
|
|
|
+ true,
|
|
|
|
|
+ subscription.auto_purchase_package_id || packages[0]?.id,
|
|
|
|
|
+ parseInt(value)
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <SelectTrigger className="bg-slate-700 border-slate-600">
|
|
|
|
|
+ <SelectValue />
|
|
|
|
|
+ </SelectTrigger>
|
|
|
|
|
+ <SelectContent className="bg-slate-800 border-slate-600">
|
|
|
|
|
+ {[1, 2, 5, 10, 15, 20].map(mins => (
|
|
|
|
|
+ <SelectItem key={mins} value={String(mins)} className="text-white">
|
|
|
|
|
+ {mins} {t('billing.minutesRemaining')}
|
|
|
|
|
+ </SelectItem>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </SelectContent>
|
|
|
|
|
+ </Select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Info Section */}
|
|
|
|
|
+ <Card className="border border-slate-700 bg-slate-800/50">
|
|
|
|
|
+ <CardContent className="p-6">
|
|
|
|
|
+ <h3 className="font-semibold text-white mb-4 flex items-center gap-2">
|
|
|
|
|
+ <Info className="w-5 h-5 text-cyan-400" />
|
|
|
|
|
+ {t('billing.topupsInfo')}
|
|
|
|
|
+ </h3>
|
|
|
|
|
+ <div className="space-y-3 text-sm text-slate-400">
|
|
|
|
|
+ <p>
|
|
|
|
|
+ <strong className="text-slate-300">{t('billing.packageExpiry')}</strong>{' '}
|
|
|
|
|
+ {t('billing.packageExpiryDesc')}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p>
|
|
|
|
|
+ <strong className="text-slate-300">{t('billing.usageOrder')}</strong>{' '}
|
|
|
|
|
+ {t('billing.usageOrderDesc')}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </main>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </SidebarProvider>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|