| @@ -28,8 +28,9 @@ | ||
| 28 | 28 | import { Table as SharedTable } from "../components/shared/Table"; |
| 29 | 29 | import { SearchFilterToolbar, BulkActionToolbar } from "../components/shared"; |
| 30 | 30 | import { apiClient } from "../lib/api-client"; |
| 31 | 31 | import { __ } from "../lib/i18n"; |
| 32 | +import { toDateValue } from "../lib/dateFormat"; | |
| 32 | 33 | import { formatYatraMoney } from "../lib/currency-display"; |
| 33 | 34 | import PremiumUpgradeCard from "./premium-pages/DynamicPricing"; |
| 34 | 35 | import { |
| 35 | 36 | ResponsiveContainer, |
| @@ -63,8 +64,13 @@ | ||
| 63 | 64 | }; |
| 64 | 65 | |
| 65 | 66 | // Main Component |
| 66 | 67 | const DynamicPricingPage: React.FC = () => { |
| 68 | + // Pro gate is a stable, server-injected flag — but we compute it once | |
| 69 | + // at the top so every hook below can read it and the early return can | |
| 70 | + // live below all hook calls (rules-of-hooks). | |
| 71 | + const moduleAvailable = isModuleAvailable(); | |
| 72 | + | |
| 67 | 73 | const [activeTab, setActiveTab] = useState("rules"); |
| 68 | 74 | const [showRuleTypeModal, setShowRuleTypeModal] = useState(false); |
| 69 | 75 | const [settings, setSettings] = useState({ |
| 70 | 76 | rule_priority_mode: "highest", |
| @@ -144,11 +150,8 @@ | ||
| 144 | 150 | const baseUrl = window.location.href.split("&action=")[0]; |
| 145 | 151 | window.location.href = `${baseUrl}&action=create-pricing-rule&rule_type=${ruleType}`; |
| 146 | 152 | }; |
| 147 | 153 | |
| 148 | - // Show premium upgrade content if module is not available | |
| 149 | - if (!isModuleAvailable()) return <PremiumUpgradeCard />; | |
| 150 | - | |
| 151 | 154 | // Fetch settings from backend |
| 152 | 155 | const { data: settingsData } = useQuery({ |
| 153 | 156 | queryKey: ["dynamic-pricing-settings"], |
| 154 | 157 | queryFn: async () => { |
| @@ -156,8 +159,9 @@ | ||
| 156 | 159 | const body = (response as any)?.data ?? response; |
| 157 | 160 | const payload = (body as any)?.data ?? body; |
| 158 | 161 | return payload && typeof payload === "object" ? payload : {}; |
| 159 | 162 | }, |
| 163 | + enabled: moduleAvailable, | |
| 160 | 164 | }); |
| 161 | 165 | |
| 162 | 166 | // Update settings state when data is loaded |
| 163 | 167 | React.useEffect(() => { |
| @@ -180,8 +184,9 @@ | ||
| 180 | 184 | const response = await apiClient.get("/dynamic-pricing/rules"); |
| 181 | 185 | |
| 182 | 186 | return response; |
| 183 | 187 | }, |
| 188 | + enabled: moduleAvailable, | |
| 184 | 189 | }); |
| 185 | 190 | |
| 186 | 191 | // Fetch statistics |
| 187 | 192 | const { |
| @@ -194,8 +199,9 @@ | ||
| 194 | 199 | const response = await apiClient.get("/dynamic-pricing/statistics"); |
| 195 | 200 | |
| 196 | 201 | return response; |
| 197 | 202 | }, |
| 203 | + enabled: moduleAvailable, | |
| 198 | 204 | }); |
| 199 | 205 | |
| 200 | 206 | // The API client returns the decoded JSON body. |
| 201 | 207 | // Some endpoints return { data: {...} } while others might return { data: { data: {...} } }. |
| @@ -203,8 +209,9 @@ | ||
| 203 | 209 | (rulesData as any)?.data?.data ?? (rulesData as any)?.data ?? []; |
| 204 | 210 | const statsPayload = |
| 205 | 211 | (statsData as any)?.data?.data ?? (statsData as any)?.data ?? {}; |
| 206 | 212 | |
| 213 | + // eslint-disable-next-line react-hooks/exhaustive-deps | |
| 207 | 214 | const rules = rulesPayload || []; |
| 208 | 215 | const stats = statsPayload || {}; |
| 209 | 216 | |
| 210 | 217 | const globalCurrency = (window as any)?.yatraAdmin?.currency || "USD"; |
| @@ -217,9 +224,9 @@ | ||
| 217 | 224 | (stats as any)?.pricing_history_trend_last_30_days, |
| 218 | 225 | ) |
| 219 | 226 | ? (stats as any).pricing_history_trend_last_30_days.map((row: any) => { |
| 220 | 227 | const day = String(row.day || ""); |
| 221 | - const dateLabel = day ? new Date(day).toLocaleDateString() : ""; | |
| 228 | + const dateLabel = day ? toDateValue(day).toLocaleDateString() : ""; | |
| 222 | 229 | return { |
| 223 | 230 | day, |
| 224 | 231 | dateLabel, |
| 225 | 232 | events: Number(row.events) || 0, |
| @@ -341,8 +348,11 @@ | ||
| 341 | 348 | } |
| 342 | 349 | bulkMutation.mutate({ action: bulkAction, ids: selectedIds }); |
| 343 | 350 | }; |
| 344 | 351 | |
| 352 | + // Gate after every hook is registered so hook order stays consistent. | |
| 353 | + if (!moduleAvailable) return <PremiumUpgradeCard />; | |
| 354 | + | |
| 345 | 355 | return ( |
| 346 | 356 | <div className="space-y-6"> |
| 347 | 357 | <PageHeader |
| 348 | 358 | title={__("Dynamic Pricing")} |
| @@ -934,12 +944,9 @@ | ||
| 934 | 944 | setConfirmDialog((prev) => ({ |
| 935 | 945 | ...prev, |
| 936 | 946 | isLoading: false, |
| 937 | 947 | })); |
| 938 | - showToast( | |
| 939 | - __("Failed to delete rule"), | |
| 940 | - "error", | |
| 941 | - ); | |
| 948 | + showToast(__("Failed to delete rule"), "error"); | |
| 942 | 949 | } |
| 943 | 950 | }, |
| 944 | 951 | }); |
| 945 | 952 | }, |
| @@ -1310,9 +1317,9 @@ | ||
| 1310 | 1317 | <XAxis |
| 1311 | 1318 | dataKey="day" |
| 1312 | 1319 | tick={{ fontSize: 12 }} |
| 1313 | 1320 | tickFormatter={(v: any) => |
| 1314 | - v ? new Date(String(v)).toLocaleDateString() : "" | |
| 1321 | + v ? toDateValue(String(v)).toLocaleDateString() : "" | |
| 1315 | 1322 | } |
| 1316 | 1323 | /> |
| 1317 | 1324 | <YAxis |
| 1318 | 1325 | tick={{ fontSize: 12 }} |
| @@ -1376,9 +1383,9 @@ | ||
| 1376 | 1383 | className="border-b border-gray-100 dark:border-gray-800" |
| 1377 | 1384 | > |
| 1378 | 1385 | <td className="py-3 px-4 text-gray-900 dark:text-white"> |
| 1379 | 1386 | {row.day |
| 1380 | - ? new Date( | |
| 1387 | + ? toDateValue( | |
| 1381 | 1388 | String(row.day), |
| 1382 | 1389 | ).toLocaleDateString() |
| 1383 | 1390 | : ""} |
| 1384 | 1391 | </td> |