- Deleted migration file that removed cached profile fields from the users table, centralizing profile data retrieval from WHMCS. - Updated CsrfMiddleware to include new public authentication endpoints for password reset, setting password, and WHMCS account linking. - Enhanced error handling in password and WHMCS linking workflows to provide clearer feedback on missing mappings and improve user experience. - Adjusted user creation and update methods in UsersFacade to handle cases where WHMCS mappings are not yet available, ensuring smoother account setup.
174 lines
6.9 KiB
TypeScript
174 lines
6.9 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState } from "react";
|
|
import { XMarkIcon, PlusIcon, ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
|
import { apiClient } from "@/lib/api";
|
|
|
|
interface TopUpModalProps {
|
|
subscriptionId: number;
|
|
onClose: () => void;
|
|
onSuccess: () => void;
|
|
onError: (message: string) => void;
|
|
}
|
|
|
|
export function TopUpModal({ subscriptionId, onClose, onSuccess, onError }: TopUpModalProps) {
|
|
const [gbAmount, setGbAmount] = useState<string>("1");
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const getCurrentAmountMb = () => {
|
|
const gb = parseInt(gbAmount, 10);
|
|
return isNaN(gb) ? 0 : gb * 1000;
|
|
};
|
|
|
|
const isValidAmount = () => {
|
|
const gb = Number(gbAmount);
|
|
return Number.isInteger(gb) && gb >= 1 && gb <= 50; // 1-50 GB, whole numbers only (Freebit API limit)
|
|
};
|
|
|
|
const calculateCost = () => {
|
|
const gb = parseInt(gbAmount, 10);
|
|
return isNaN(gb) ? 0 : gb * 500; // 1GB = 500 JPY
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (!isValidAmount()) {
|
|
onError("Please enter a whole number between 1 GB and 100 GB");
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
|
|
try {
|
|
const requestBody = {
|
|
quotaMb: getCurrentAmountMb(),
|
|
amount: calculateCost(),
|
|
currency: "JPY",
|
|
};
|
|
|
|
await apiClient.POST("/api/subscriptions/{id}/sim/top-up", {
|
|
params: { path: { id: subscriptionId } },
|
|
body: requestBody,
|
|
});
|
|
|
|
onSuccess();
|
|
} catch (error: unknown) {
|
|
onError(error instanceof Error ? error.message : "Failed to top up SIM");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleBackdropClick = (e: React.MouseEvent) => {
|
|
if (e.target === e.currentTarget) {
|
|
onClose();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 overflow-y-auto" onClick={handleBackdropClick}>
|
|
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
|
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
|
|
|
|
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
|
{/* Header */}
|
|
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="flex items-center">
|
|
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
|
|
<PlusIcon className="h-6 w-6 text-blue-600" />
|
|
</div>
|
|
<div className="ml-4">
|
|
<h3 className="text-lg leading-6 font-medium text-gray-900">Top Up Data</h3>
|
|
<p className="text-sm text-gray-500">Add data quota to your SIM service</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="text-gray-400 hover:text-gray-500 focus:outline-none"
|
|
>
|
|
<XMarkIcon className="h-6 w-6" />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={e => void handleSubmit(e)}>
|
|
{/* Amount Input */}
|
|
<div className="mb-6">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">Amount (GB)</label>
|
|
<div className="relative">
|
|
<input
|
|
type="number"
|
|
value={gbAmount}
|
|
onChange={e => setGbAmount(e.target.value)}
|
|
placeholder="Enter amount in GB"
|
|
min="1"
|
|
max="50"
|
|
step="1"
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 pr-12"
|
|
/>
|
|
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
|
<span className="text-gray-500 text-sm">GB</span>
|
|
</div>
|
|
</div>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
Enter the amount of data you want to add (1 - 50 GB, whole numbers)
|
|
</p>
|
|
</div>
|
|
|
|
{/* Cost Display */}
|
|
<div className="mb-6 p-4 bg-blue-50 rounded-lg border border-blue-200">
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<div className="text-sm font-medium text-blue-900">
|
|
{gbAmount && !isNaN(parseInt(gbAmount, 10)) ? `${gbAmount} GB` : "0 GB"}
|
|
</div>
|
|
<div className="text-xs text-blue-700">= {getCurrentAmountMb()} MB</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<div className="text-lg font-bold text-blue-900">
|
|
¥{calculateCost().toLocaleString()}
|
|
</div>
|
|
<div className="text-xs text-blue-700">(1GB = ¥500)</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Validation Warning */}
|
|
{!isValidAmount() && gbAmount && (
|
|
<div className="mb-4 bg-red-50 border border-red-200 rounded-lg p-3">
|
|
<div className="flex items-center">
|
|
<ExclamationTriangleIcon className="h-4 w-4 text-red-500 mr-2" />
|
|
<p className="text-sm text-red-800">
|
|
Amount must be a whole number between 1 GB and 50 GB
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-3 space-y-3 space-y-reverse sm:space-y-0">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
disabled={loading}
|
|
className="w-full sm:w-auto px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
|
>
|
|
Back
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={loading || !isValidAmount()}
|
|
className="w-full sm:w-auto px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
|
>
|
|
{loading ? "Processing..." : `Top Up Now - ¥${calculateCost().toLocaleString()}`}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|