PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.5
Yatra – Travel Booking & Tour Operator Software v3.0.5
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / resources / js / pages / DiscountForm.tsx

DiscountForm.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.5, at resources/js/pages/DiscountForm.tsx

2,705 lines 124.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Discount Form Page
3 * Add/Edit Discount Coupon form
4 */
5
6 import React, { useState, useEffect, useMemo } from "react";
7 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
8 import {
9 ArrowLeft,
10 Save,
11 Loader2,
12 Info,
13 Plus,
14 Trash2,
15 Users,
16 } from "lucide-react";
17 import { __ } from "../lib/i18n";
18 import { usePermissions } from "../hooks/usePermissions";
19 import { useToast } from "../components/ui/toast";
20 import { apiClient } from "../lib/api-client";
21 import { Button } from "../components/ui/button";
22 import { Input } from "../components/ui/input";
23 import { Select } from "../components/ui/select";
24 import { PageHeader } from "../components/common/PageHeader";
25 import {
26 Card,
27 CardContent,
28 CardHeader,
29 CardTitle,
30 } from "../components/ui/card";
31 import { ConditionalRender } from "../components/ui/conditional-render";
32 import { HelpText } from "../components/ui/help-text";
33 import { DatePicker } from "../components/ui/date-picker";
34 import { ApplicableTripSelector } from "../components/shared/ApplicableTripSelector";
35 import { PremiumUpgradeDialog } from "../components/modules/PremiumUpgradeDialog";
36
37 interface DiscountFormData {
38 code: string;
39 description: string;
40 type: "percentage" | "fixed";
41 amount: string;
42 max_discount_amount: string; // Maximum discount amount for percentage discounts
43 usage_limit: string; // Total usage limit
44 usage_limit_per_customer: string; // Usage limit per customer
45 valid_from: string; // Start date
46 expiry_date: string; // End date
47 status: "draft" | "publish" | "trash";
48 applicable_to: "all" | "specific_trips";
49 trip_ids: number[];
50 min_amount: string; // Minimum booking amount
51 first_time_customer_only: boolean; // Only for first-time customers
52 is_group_discount: boolean;
53 discount_mode: "promo" | "group" | "both"; // Type of discount: promo code only, group only, or both
54 group_discount_mode: "total" | "category_based";
55 group_discount_ranges: Array<{
56 id: string;
57 min_group_size: string;
58 max_group_size: string; // empty = unlimited
59 discount_type: "percentage" | "fixed";
60 discount_amount: string;
61 categories: Array<{
62 id: string;
63 traveler_category_id: string;
64 discount_type: "percentage" | "fixed";
65 discount_amount: string;
66 }>;
67 }>;
68 category_discounts: Array<{
69 traveler_category_id: string;
70 traveler_category_label: string;
71 isSelecting?: boolean;
72 ranges: Array<{
73 id: string;
74 min_group_size: string;
75 max_group_size: string; // empty = unlimited
76 discount_type: "percentage" | "fixed";
77 discount_amount: string;
78 }>;
79 }>;
80 }
81
82 const DiscountForm: React.FC = () => {
83 const queryClient = useQueryClient();
84 const { can, permissions, isPro } = usePermissions();
85 const { showToast } = useToast();
86 const [showPremiumModal, setShowPremiumModal] = useState(false);
87 // Get initial discount_mode from URL at initialization time
88 const getInitialState = (): DiscountFormData => {
89 const params = new URLSearchParams(window.location.search);
90 const urlDiscountMode = (params.get("discount_mode") || "both") as
91 | "promo"
92 | "group"
93 | "both";
94 const isGroupOnly = urlDiscountMode === "group";
95
96 return {
97 code: "",
98 description: "",
99 type: "percentage",
100 amount: isGroupOnly ? "0" : "",
101 max_discount_amount: "",
102 usage_limit: "0",
103 usage_limit_per_customer: "0",
104 valid_from: "",
105 expiry_date: "",
106 status: "draft",
107 applicable_to: "all",
108 trip_ids: [],
109 min_amount: "",
110 first_time_customer_only: false,
111 is_group_discount: isGroupOnly,
112 discount_mode: urlDiscountMode,
113 group_discount_mode: "total",
114 group_discount_ranges: [
115 {
116 id: String(Date.now()),
117 min_group_size: "",
118 max_group_size: "",
119 discount_type: "percentage",
120 discount_amount: "",
121 categories: [],
122 },
123 ],
124 category_discounts: [],
125 };
126 };
127
128 const [formData, setFormData] = useState<DiscountFormData>(getInitialState);
129 const [errors, setErrors] = useState<Record<string, string>>({});
130 const [isSubmitting, setIsSubmitting] = useState(false);
131 const [showCategoryDropdown, setShowCategoryDropdown] = useState(false);
132
133 // Get action and id from URL
134 const action = useMemo(() => {
135 const params = new URLSearchParams(window.location.search);
136 return params.get("action") || "create";
137 }, []);
138
139 const discountId = useMemo(() => {
140 const params = new URLSearchParams(window.location.search);
141 return params.get("id") ? parseInt(params.get("id") || "0") : null;
142 }, []);
143
144 const duplicateId = useMemo(() => {
145 const params = new URLSearchParams(window.location.search);
146 return params.get("duplicate")
147 ? parseInt(params.get("duplicate") || "0")
148 : null;
149 }, []);
150
151 // Get discount_mode from URL (promo, group, or both) - compute once at module level
152 const discountMode = useMemo(() => {
153 const params = new URLSearchParams(window.location.search);
154 return (params.get("discount_mode") || "both") as
155 | "promo"
156 | "group"
157 | "both";
158 }, []);
159
160 const isEditMode = action === "edit" && discountId !== null;
161 const isDuplicateMode = action === "create" && duplicateId !== null;
162
163 // For new discounts, use URL param; for edit mode, we'll use formData.discount_mode after it's loaded
164 const isGroupOnlyMode = discountMode === "group";
165 const isPromoOnlyMode = discountMode === "promo";
166
167 // Computed mode that works for both create and edit - use formData when available
168 const effectiveDiscountMode = isEditMode
169 ? formData.discount_mode
170 : discountMode;
171 const isEffectiveGroupOnly = effectiveDiscountMode === "group";
172 const isEffectivePromoOnly = effectiveDiscountMode === "promo";
173
174 // Fetch discount data if editing or duplicating
175 const { data: discountData, isLoading: isLoadingDiscount } = useQuery({
176 queryKey: ["discount", discountId || duplicateId],
177 queryFn: async () => {
178 const id = discountId || duplicateId;
179 if (!id) return null;
180 try {
181 const response = await apiClient.get(`/discounts/${id}`);
182 return response;
183 } catch (error: any) {
184 showToast(
185 error?.message || __("Failed to load discount", "yatra"),
186 "error",
187 );
188 throw error;
189 }
190 },
191 enabled: (isEditMode || isDuplicateMode) && can("yatra_manage_discounts"),
192 });
193
194 // Load discount data into form when editing or duplicating
195 useEffect(() => {
196 if (discountData && (isEditMode || isDuplicateMode)) {
197 setFormData({
198 code: isDuplicateMode
199 ? `${discountData.code}_COPY`
200 : discountData.code || "",
201 description: discountData.description || "",
202 type: (discountData.type || "percentage") as "percentage" | "fixed",
203 amount: discountData.amount?.toString() || "",
204 max_discount_amount: discountData.max_discount_amount?.toString() || "",
205 usage_limit: discountData.usage_limit?.toString() || "0",
206 usage_limit_per_customer:
207 discountData.usage_limit_per_customer?.toString() || "0",
208 valid_from: discountData.valid_from || "",
209 expiry_date: discountData.expiry_date || "",
210 status: (isDuplicateMode ? "draft" : discountData.status || "draft") as
211 | "draft"
212 | "publish"
213 | "trash",
214 applicable_to: (discountData.applicable_to || "all") as
215 | "all"
216 | "specific_trips",
217 trip_ids: discountData.trip_ids || [],
218 min_amount: discountData.min_amount?.toString() || "",
219 first_time_customer_only:
220 discountData.first_time_customer_only || false,
221 is_group_discount: discountData.is_group_discount || false,
222 discount_mode: (discountData.discount_mode || "both") as
223 | "promo"
224 | "group"
225 | "both",
226 group_discount_mode: (discountData.group_discount_mode || "total") as
227 | "total"
228 | "category_based",
229 group_discount_ranges:
230 Array.isArray(discountData.group_discount_ranges) &&
231 discountData.group_discount_ranges.length
232 ? discountData.group_discount_ranges
233 : [
234 {
235 id: String(Date.now()),
236 min_group_size: discountData.min_group_size?.toString() || "",
237 max_group_size: discountData.max_group_size?.toString() || "",
238 discount_type: (discountData.group_discount_type ||
239 "percentage") as "percentage" | "fixed",
240 discount_amount:
241 discountData.group_discount_amount?.toString() || "",
242 categories: [],
243 },
244 ],
245 category_discounts:
246 Array.isArray(discountData.category_discounts) &&
247 discountData.category_discounts?.length
248 ? discountData.category_discounts
249 : [],
250 });
251 }
252 }, [discountData, isEditMode, isDuplicateMode]);
253
254 // Set initial form state based on discount mode (only for new discounts)
255 useEffect(() => {
256 if (!isEditMode && !isDuplicateMode && !discountData) {
257 if (isGroupOnlyMode) {
258 // Group discount only - auto-enable group discount, set amount to 0
259 setFormData((prev) => ({
260 ...prev,
261 is_group_discount: true,
262 discount_mode: "group",
263 amount: "0",
264 type: "percentage",
265 }));
266 } else if (isPromoOnlyMode) {
267 // Promo code only - disable group discount
268 setFormData((prev) => ({
269 ...prev,
270 is_group_discount: false,
271 discount_mode: "promo",
272 }));
273 } else {
274 // Both - default
275 setFormData((prev) => ({
276 ...prev,
277 discount_mode: "both",
278 }));
279 }
280 }
281 }, [
282 isGroupOnlyMode,
283 isPromoOnlyMode,
284 isEditMode,
285 isDuplicateMode,
286 discountData,
287 ]);
288
289 const handleFieldChange = (field: keyof DiscountFormData, value: any) => {
290 setFormData((prev) => ({ ...prev, [field]: value }));
291 if (errors[field]) {
292 setErrors((prev) => ({ ...prev, [field]: "" }));
293 }
294
295 // Clear ranges when switching discount modes
296 if (field === "group_discount_mode") {
297 if (value === "category_based") {
298 // Switch to category-based - clear regular ranges and start with empty categories
299 setFormData((prev) => ({
300 ...prev,
301 group_discount_mode: "category_based",
302 group_discount_ranges: [],
303 category_discounts: [], // Ensure empty categories array
304 }));
305 } else {
306 // Switch to total - clear category discounts and start with one empty range
307 setFormData((prev) => ({
308 ...prev,
309 group_discount_mode: "total",
310 group_discount_ranges: [
311 {
312 id: String(Date.now()),
313 min_group_size: "",
314 max_group_size: "",
315 discount_type: "percentage",
316 discount_amount: "",
317 categories: [],
318 },
319 ],
320 category_discounts: [], // Ensure empty categories array
321 }));
322 }
323 }
324 };
325
326 const handleGroupDiscountToggle = (checked: boolean) => {
327 handleFieldChange("is_group_discount", checked);
328 };
329
330 const travelerCategoriesQuery = useQuery({
331 queryKey: ["traveler-categories"],
332 queryFn: async () => {
333 try {
334 const response = await apiClient.get("/traveler-categories", {
335 params: {
336 per_page: 100,
337 status: "publish",
338 },
339 });
340 const categories =
341 response?.data?.data || response?.data || response || [];
342 return Array.isArray(categories) ? categories : [];
343 } catch (error: any) {
344 console.error("Failed to load traveler categories:", error);
345 return [];
346 }
347 },
348 staleTime: 5 * 60 * 1000,
349 });
350
351 const travelerCategoryOptions = useMemo(() => {
352 const items = travelerCategoriesQuery.data || [];
353 return items
354 .filter(
355 (c: any) =>
356 !!c &&
357 (typeof c.id === "number" || typeof c.id === "string") &&
358 (c.status === "publish" || c.status === "active"),
359 )
360 .map((c: any) => ({
361 value: String(c.id),
362 label: c.label || c.slug || String(c.id),
363 description: c.description || "",
364 age_min: c.age_min,
365 age_max: c.age_max,
366 pricing_mode: c.pricing_mode || "per_person",
367 }));
368 }, [travelerCategoriesQuery.data]);
369
370 const addRange = () => {
371 setFormData((prev) => ({
372 ...prev,
373 group_discount_ranges: [
374 ...prev.group_discount_ranges,
375 {
376 id: String(Date.now() + Math.random()),
377 min_group_size: "",
378 max_group_size: "",
379 discount_type: "percentage",
380 discount_amount: "",
381 categories: [],
382 },
383 ],
384 }));
385 };
386
387 const removeRange = (rangeId: string) => {
388 setFormData((prev) => ({
389 ...prev,
390 group_discount_ranges: prev.group_discount_ranges.filter(
391 (r) => r.id !== rangeId,
392 ),
393 }));
394 };
395
396 const updateRange = (
397 rangeId: string,
398 patch: Partial<DiscountFormData["group_discount_ranges"][number]>,
399 ) => {
400 setFormData((prev) => ({
401 ...prev,
402 group_discount_ranges: prev.group_discount_ranges.map((r) =>
403 r.id === rangeId ? { ...r, ...patch } : r,
404 ),
405 }));
406 };
407
408 // Category-based discount functions
409 const addTravelerCategory = () => {
410 // Toggle category selection dropdown
411 setShowCategoryDropdown((prev) => !prev);
412 };
413
414 const selectCategory = (categoryId: string, categoryLabel: string) => {
415 // Add selected category and close dropdown
416 setFormData((prev) => ({
417 ...prev,
418 category_discounts: [
419 ...prev.category_discounts,
420 {
421 traveler_category_id: categoryId,
422 traveler_category_label: categoryLabel,
423 ranges: [],
424 isSelecting: false,
425 },
426 ],
427 }));
428 setShowCategoryDropdown(false);
429 };
430
431 const removeTravelerCategory = (categoryIndex: number) => {
432 setFormData((prev) => ({
433 ...prev,
434 category_discounts: prev.category_discounts.filter(
435 (_, idx) => idx !== categoryIndex,
436 ),
437 }));
438 };
439
440 const updateTravelerCategory = (
441 categoryIndex: number,
442 patch: Partial<DiscountFormData["category_discounts"][number]>,
443 ) => {
444 setFormData((prev) => ({
445 ...prev,
446 category_discounts: prev.category_discounts.map((cat, idx) =>
447 idx === categoryIndex ? { ...cat, ...patch } : cat,
448 ),
449 }));
450 };
451
452 const addRangeToCategory = (categoryIndex: number) => {
453 setFormData((prev) => ({
454 ...prev,
455 category_discounts: prev.category_discounts.map((cat, idx) => {
456 if (idx !== categoryIndex) return cat;
457 return {
458 ...cat,
459 ranges: [
460 ...cat.ranges,
461 {
462 id: String(Date.now() + Math.random()),
463 min_group_size: "",
464 max_group_size: "",
465 discount_type: "percentage",
466 discount_amount: "",
467 },
468 ],
469 };
470 }),
471 }));
472 };
473
474 const removeRangeFromCategory = (categoryIndex: number, rangeId: string) => {
475 setFormData((prev) => ({
476 ...prev,
477 category_discounts: prev.category_discounts.map((cat, idx) => {
478 if (idx !== categoryIndex) return cat;
479 return { ...cat, ranges: cat.ranges.filter((r) => r.id !== rangeId) };
480 }),
481 }));
482 };
483
484 const updateRangeInCategory = (
485 categoryIndex: number,
486 rangeId: string,
487 patch: Partial<
488 DiscountFormData["category_discounts"][number]["ranges"][number]
489 >,
490 ) => {
491 setFormData((prev) => ({
492 ...prev,
493 category_discounts: prev.category_discounts.map((cat, idx) => {
494 if (idx !== categoryIndex) return cat;
495 const ranges = cat.ranges.map((r) =>
496 r.id === rangeId ? { ...r, ...patch } : r,
497 );
498 return { ...cat, ranges };
499 }),
500 }));
501 };
502
503 const validateForm = (): boolean => {
504 const newErrors: Record<string, string> = {};
505
506 // Internal name/code is always required
507 if (!formData.code.trim()) {
508 newErrors.code = isEffectiveGroupOnly
509 ? __("Internal name is required", "yatra")
510 : __("Coupon code is required", "yatra");
511 } else if (!/^[A-Z0-9_-]+$/.test(formData.code)) {
512 newErrors.code = __(
513 "Only uppercase letters, numbers, hyphens, and underscores are allowed",
514 "yatra",
515 );
516 }
517
518 // Discount amount validation - skip for group-only mode
519 if (!isEffectiveGroupOnly) {
520 if (!formData.amount.trim()) {
521 newErrors.amount = __("Discount amount is required", "yatra");
522 } else {
523 const amount = parseFloat(formData.amount);
524 if (isNaN(amount) || amount < 0) {
525 newErrors.amount = __(
526 "Discount amount must be a positive number",
527 "yatra",
528 );
529 } else if (formData.type === "percentage" && amount > 100) {
530 newErrors.amount = __(
531 "Percentage discount cannot exceed 100%",
532 "yatra",
533 );
534 }
535 }
536
537 // Maximum discount amount validation (for percentage discounts)
538 if (
539 formData.type === "percentage" &&
540 formData.max_discount_amount &&
541 formData.max_discount_amount.trim()
542 ) {
543 const maxAmount = parseFloat(formData.max_discount_amount);
544 if (isNaN(maxAmount) || maxAmount <= 0) {
545 newErrors.max_discount_amount = __(
546 "Maximum discount amount must be a positive number",
547 "yatra",
548 );
549 }
550 }
551 }
552
553 if (formData.usage_limit && formData.usage_limit !== "0") {
554 const limit = parseInt(formData.usage_limit);
555 if (isNaN(limit) || limit < 0) {
556 newErrors.usage_limit = __(
557 "Usage limit must be a positive number or 0 for unlimited",
558 "yatra",
559 );
560 }
561 }
562
563 if (
564 formData.usage_limit_per_customer &&
565 formData.usage_limit_per_customer !== "0"
566 ) {
567 const limit = parseInt(formData.usage_limit_per_customer);
568 if (isNaN(limit) || limit < 0) {
569 newErrors.usage_limit_per_customer = __(
570 "Per-customer usage limit must be a positive number or 0 for unlimited",
571 "yatra",
572 );
573 }
574 }
575
576 // Date validation
577 const today = new Date();
578 today.setHours(0, 0, 0, 0);
579
580 // Valid from date can be in the past (backdating allowed)
581 // No validation needed for valid_from date
582
583 if (formData.expiry_date) {
584 const expiryDate = new Date(formData.expiry_date);
585 if (expiryDate < today) {
586 newErrors.expiry_date = __(
587 "Expiry date cannot be in the past",
588 "yatra",
589 );
590 }
591 }
592
593 // Check if expiry date is after valid from date
594 if (formData.valid_from && formData.expiry_date) {
595 const validFrom = new Date(formData.valid_from);
596 const expiryDate = new Date(formData.expiry_date);
597 if (expiryDate < validFrom) {
598 newErrors.expiry_date = __(
599 "Expiry date must be after the valid from date",
600 "yatra",
601 );
602 }
603 }
604
605 if (formData.min_amount && formData.min_amount.trim()) {
606 const minAmount = parseFloat(formData.min_amount);
607 if (isNaN(minAmount) || minAmount < 0) {
608 newErrors.min_amount = __(
609 "Minimum amount must be a positive number",
610 "yatra",
611 );
612 }
613 }
614
615 // Group discount validation (multiple ranges) - only for 'total' mode
616 if (
617 formData.is_group_discount &&
618 formData.group_discount_mode === "total"
619 ) {
620 if (
621 !formData.group_discount_ranges ||
622 formData.group_discount_ranges.length === 0
623 ) {
624 newErrors.group_discount_ranges = __(
625 "At least one group discount range is required",
626 "yatra",
627 );
628 } else {
629 formData.group_discount_ranges.forEach((range, idx) => {
630 const prefix = `group_discount_ranges_${idx}`;
631 const min = parseInt(range.min_group_size);
632 const max = range.max_group_size
633 ? parseInt(range.max_group_size)
634 : null;
635
636 if (Number.isNaN(min) || min < 1) {
637 newErrors[`${prefix}_min`] = __(
638 "Minimum group size must be at least 1",
639 "yatra",
640 );
641 }
642
643 if (max !== null) {
644 if (Number.isNaN(max) || max < 1) {
645 newErrors[`${prefix}_max`] = __(
646 "Maximum group size must be a positive number",
647 "yatra",
648 );
649 } else if (!Number.isNaN(min) && max <= min) {
650 newErrors[`${prefix}_max`] = __(
651 "Maximum group size must be greater than minimum group size",
652 "yatra",
653 );
654 }
655 }
656
657 const amount = range.discount_amount
658 ? parseFloat(range.discount_amount)
659 : NaN;
660 if (!range.discount_type) {
661 newErrors[`${prefix}_type`] = __(
662 "Discount type is required",
663 "yatra",
664 );
665 }
666 if (Number.isNaN(amount) || amount <= 0) {
667 newErrors[`${prefix}_amount`] = __(
668 "Discount amount must be a positive number",
669 "yatra",
670 );
671 } else if (range.discount_type === "percentage" && amount > 100) {
672 newErrors[`${prefix}_amount`] = __(
673 "Percentage discount cannot exceed 100%",
674 "yatra",
675 );
676 }
677 });
678 }
679 }
680
681 // Category-based discounts validation
682 if (
683 formData.is_group_discount &&
684 formData.group_discount_mode === "category_based"
685 ) {
686 if (
687 !formData.category_discounts ||
688 formData.category_discounts.length === 0
689 ) {
690 newErrors.category_discounts = __(
691 "Add at least one traveler category",
692 "yatra",
693 );
694 } else {
695 formData.category_discounts.forEach((cat, catIdx) => {
696 const catPrefix = `category_discounts_${catIdx}`;
697 if (!cat.traveler_category_id) {
698 newErrors[`${catPrefix}_id`] = __(
699 "Traveler category is required",
700 "yatra",
701 );
702 }
703 if (!cat.ranges || cat.ranges.length === 0) {
704 newErrors[`${catPrefix}_ranges`] = __(
705 "Add at least one discount range",
706 "yatra",
707 );
708 } else {
709 cat.ranges.forEach((range, rIdx) => {
710 const rangePrefix = `${catPrefix}_ranges_${rIdx}`;
711 const min = parseInt(range.min_group_size);
712 const max = range.max_group_size
713 ? parseInt(range.max_group_size)
714 : null;
715
716 if (Number.isNaN(min) || min < 1) {
717 newErrors[`${rangePrefix}_min`] = __(
718 "Minimum group size must be at least 1",
719 "yatra",
720 );
721 }
722
723 if (max !== null) {
724 if (Number.isNaN(max) || max < 1) {
725 newErrors[`${rangePrefix}_max`] = __(
726 "Maximum group size must be a positive number",
727 "yatra",
728 );
729 } else if (!Number.isNaN(min) && max <= min) {
730 newErrors[`${rangePrefix}_max`] = __(
731 "Maximum group size must be greater than minimum group size",
732 "yatra",
733 );
734 }
735 }
736
737 const amount = range.discount_amount
738 ? parseFloat(range.discount_amount)
739 : NaN;
740 if (!range.discount_type) {
741 newErrors[`${rangePrefix}_type`] = __(
742 "Discount type is required",
743 "yatra",
744 );
745 }
746 if (Number.isNaN(amount) || amount <= 0) {
747 newErrors[`${rangePrefix}_amount`] = __(
748 "Discount amount must be a positive number",
749 "yatra",
750 );
751 } else if (range.discount_type === "percentage" && amount > 100) {
752 newErrors[`${rangePrefix}_amount`] = __(
753 "Percentage discount cannot exceed 100%",
754 "yatra",
755 );
756 }
757 });
758 }
759 });
760 }
761 }
762
763 setErrors(newErrors);
764
765 // Scroll to first error field
766 if (Object.keys(newErrors).length > 0) {
767 const firstErrorKey = Object.keys(newErrors)[0];
768 // Try to find the element by name or id
769 const errorElement = document.querySelector(
770 `[name="${firstErrorKey}"], #${firstErrorKey}, [data-field="${firstErrorKey}"]`,
771 );
772 if (errorElement) {
773 errorElement.scrollIntoView({ behavior: "smooth", block: "center" });
774 // Focus the element if it's an input
775 if (
776 errorElement instanceof HTMLInputElement ||
777 errorElement instanceof HTMLSelectElement ||
778 errorElement instanceof HTMLTextAreaElement
779 ) {
780 setTimeout(() => errorElement.focus(), 300);
781 }
782 } else {
783 // Fallback: scroll to the first error message
784 const errorMessage = document.querySelector(
785 ".text-red-500, .text-red-600",
786 );
787 if (errorMessage) {
788 errorMessage.scrollIntoView({ behavior: "smooth", block: "center" });
789 }
790 }
791 }
792
793 return Object.keys(newErrors).length === 0;
794 };
795
796 // Create/Update mutation
797 const saveMutation = useMutation({
798 mutationFn: async (data: DiscountFormData) => {
799 const payload: any = {
800 code: data.code.trim().toUpperCase(),
801 description: data.description.trim(),
802 type: data.type,
803 amount: parseFloat(data.amount) || 0,
804 max_discount_amount: data.max_discount_amount
805 ? parseFloat(data.max_discount_amount)
806 : null,
807 usage_limit: parseInt(data.usage_limit) || 0,
808 usage_limit_per_customer: parseInt(data.usage_limit_per_customer) || 0,
809 valid_from: data.valid_from || null,
810 expiry_date: data.expiry_date || null,
811 status: data.status,
812 applicable_to: data.applicable_to,
813 trip_ids: data.applicable_to === "specific_trips" ? data.trip_ids : [],
814 min_amount: data.min_amount ? parseFloat(data.min_amount) : null,
815 first_time_customer_only: data.first_time_customer_only,
816 is_group_discount: data.is_group_discount,
817 discount_mode: data.discount_mode,
818 group_discount_mode: data.is_group_discount
819 ? data.group_discount_mode
820 : null,
821 group_discount_ranges: data.is_group_discount
822 ? data.group_discount_ranges
823 : [],
824 category_discounts:
825 data.is_group_discount &&
826 data.group_discount_mode === "category_based"
827 ? data.category_discounts.map((cat) => ({
828 traveler_category_id: cat.traveler_category_id,
829 traveler_category_label: cat.traveler_category_label,
830 ranges: cat.ranges.map((range) => ({
831 id: range.id,
832 min_group_size: range.min_group_size,
833 max_group_size: range.max_group_size,
834 discount_type: range.discount_type,
835 discount_amount: range.discount_amount,
836 })),
837 }))
838 : [],
839 };
840
841 if (isEditMode && discountId) {
842 return await apiClient.put(`/discounts/${discountId}`, payload);
843 } else {
844 return await apiClient.post("/discounts", payload);
845 }
846 },
847 onSuccess: () => {
848 queryClient.invalidateQueries({ queryKey: ["discounts"] });
849 queryClient.invalidateQueries({
850 queryKey: ["discount", discountId || duplicateId],
851 });
852 showToast(
853 isEditMode
854 ? __("Discount updated successfully", "yatra")
855 : __("Discount created successfully", "yatra"),
856 "success",
857 );
858 // Only redirect to listing page on create, not on update
859 if (!isEditMode) {
860 setTimeout(() => {
861 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=discounts`;
862 }, 1000);
863 }
864 setIsSubmitting(false);
865 },
866 onError: (error: any) => {
867 const errorMessage =
868 error?.message ||
869 __("An error occurred while saving the discount", "yatra");
870 showToast(errorMessage, "error");
871 setIsSubmitting(false);
872 },
873 });
874
875 const handleSubmit = async (e: React.FormEvent) => {
876 e.preventDefault();
877
878 if (!validateForm()) {
879 return;
880 }
881
882 setIsSubmitting(true);
883 try {
884 await saveMutation.mutateAsync(formData);
885 } catch (error) {
886 // Error handled in mutation
887 } finally {
888 setIsSubmitting(false);
889 }
890 };
891
892 const handleBack = () => {
893 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=discounts`;
894 };
895
896 if (isLoadingDiscount) {
897 return (
898 <div className="space-y-3">
899 {/* Skeleton Header */}
900 <div className="flex items-center justify-between">
901 <div className="space-y-2">
902 <div className="h-8 w-64 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
903 <div className="h-4 w-48 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
904 </div>
905 <div className="h-10 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
906 </div>
907
908 {/* Skeleton Info Card */}
909 <Card className="bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800">
910 <CardContent className="p-4">
911 <div className="flex items-start gap-3">
912 <div className="w-5 h-5 bg-blue-200 dark:bg-blue-700 rounded animate-pulse" />
913 <div className="flex-1 space-y-2">
914 <div className="h-4 w-40 bg-blue-200 dark:bg-blue-700 rounded animate-pulse" />
915 <div className="space-y-1">
916 <div className="h-3 w-full bg-blue-100 dark:bg-blue-800 rounded animate-pulse" />
917 <div className="h-3 w-3/4 bg-blue-100 dark:bg-blue-800 rounded animate-pulse" />
918 </div>
919 </div>
920 </div>
921 </CardContent>
922 </Card>
923
924 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
925 {/* Main Form Skeleton */}
926 <div className="lg:col-span-2 space-y-3">
927 {/* Basic Information Card Skeleton */}
928 <Card>
929 <CardHeader className="pb-2">
930 <div className="h-5 w-40 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
931 </CardHeader>
932 <CardContent className="space-y-4">
933 {/* Coupon Code */}
934 <div className="space-y-2">
935 <div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
936 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
937 </div>
938 {/* Description */}
939 <div className="space-y-2">
940 <div className="h-4 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
941 <div className="h-20 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
942 </div>
943 {/* Type and Amount */}
944 <div className="grid grid-cols-2 gap-3">
945 <div className="space-y-2">
946 <div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
947 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
948 </div>
949 <div className="space-y-2">
950 <div className="h-4 w-28 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
951 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
952 </div>
953 </div>
954 </CardContent>
955 </Card>
956
957 {/* Usage Limits Card Skeleton */}
958 <Card>
959 <CardHeader className="pb-2">
960 <div className="h-5 w-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
961 </CardHeader>
962 <CardContent className="space-y-4">
963 <div className="grid grid-cols-2 gap-3">
964 <div className="space-y-2">
965 <div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
966 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
967 </div>
968 <div className="space-y-2">
969 <div className="h-4 w-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
970 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
971 </div>
972 </div>
973 {/* Dates */}
974 <div className="grid grid-cols-2 gap-3">
975 <div className="space-y-2">
976 <div className="h-4 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
977 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
978 </div>
979 <div className="space-y-2">
980 <div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
981 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
982 </div>
983 </div>
984 </CardContent>
985 </Card>
986
987 {/* Group Discount Card Skeleton */}
988 <Card>
989 <CardHeader className="pb-2">
990 <div className="h-5 w-44 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
991 </CardHeader>
992 <CardContent className="space-y-4">
993 <div className="flex items-center gap-3">
994 <div className="h-5 w-5 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
995 <div className="h-4 w-40 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
996 </div>
997 </CardContent>
998 </Card>
999 </div>
1000
1001 {/* Sidebar Skeleton */}
1002 <div className="space-y-3">
1003 {/* Status Card */}
1004 <Card>
1005 <CardHeader className="pb-2">
1006 <div className="h-5 w-16 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
1007 </CardHeader>
1008 <CardContent>
1009 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
1010 </CardContent>
1011 </Card>
1012
1013 {/* Submit Button */}
1014 <Card>
1015 <CardContent className="p-3">
1016 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
1017 </CardContent>
1018 </Card>
1019 </div>
1020 </div>
1021 </div>
1022 );
1023 }
1024
1025 return (
1026 <div className="space-y-3">
1027 <PageHeader
1028 title={
1029 isEditMode
1030 ? __("Edit Discount Coupon", "yatra")
1031 : __("Add New Discount Coupon", "yatra")
1032 }
1033 description={
1034 isEditMode
1035 ? __("Update discount coupon details", "yatra")
1036 : __("Create a new discount coupon for your trips", "yatra")
1037 }
1038 actions={
1039 <Button
1040 variant="outline"
1041 onClick={handleBack}
1042 className="flex items-center gap-2"
1043 >
1044 <ArrowLeft className="w-4 h-4" />
1045 {__("Back", "yatra")}
1046 </Button>
1047 }
1048 />
1049
1050 <ConditionalRender capability="yatra_manage_discounts">
1051 {/* Discount Information Card */}
1052 <Card className="bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800">
1053 <CardContent className="p-4">
1054 <div className="flex items-start gap-3">
1055 <Info className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
1056 <div className="flex-1">
1057 <h4 className="text-sm font-semibold text-blue-900 dark:text-blue-100 mb-2">
1058 {__("How Discounts Work", "yatra")}
1059 </h4>
1060 <ul className="text-xs text-blue-800 dark:text-blue-200 space-y-1.5">
1061 <li>
1062 • <strong>{__("Regular Discount", "yatra")}:</strong>{" "}
1063 {__(
1064 "Applied first to the booking total (trip price × travelers)",
1065 "yatra",
1066 )}
1067 </li>
1068 <li>
1069 • <strong>{__("Group Discount", "yatra")}:</strong>{" "}
1070 {__(
1071 "Additional discount applied AFTER regular discount when group size requirement is met",
1072 "yatra",
1073 )}
1074 </li>
1075 <li>
1076 • <strong>{__("Maximum Cap", "yatra")}:</strong>{" "}
1077 {__(
1078 "For percentage discounts, you can set a maximum dollar amount to cap the discount",
1079 "yatra",
1080 )}
1081 </li>
1082 <li>
1083 • <strong>{__("Example", "yatra")}:</strong>{" "}
1084 {__(
1085 "$1000 booking, 15% regular discount ($150 off), then 10% group discount on $850 = $85 more off. Final: $765",
1086 "yatra",
1087 )}
1088 </li>
1089 </ul>
1090 </div>
1091 </div>
1092 </CardContent>
1093 </Card>
1094
1095 <form onSubmit={handleSubmit}>
1096 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
1097 {/* Main Form Fields */}
1098 <div className="lg:col-span-2 space-y-3">
1099 {/* Discount Mode Indicator - Show on both Create and Edit */}
1100 {(() => {
1101 // Determine mode: use URL param for new discounts, formData for existing
1102 const currentMode = isEditMode
1103 ? formData.discount_mode
1104 : discountMode;
1105 const isGroupOnly = currentMode === "group";
1106 const isPromoOnly = currentMode === "promo";
1107
1108 return (
1109 <div
1110 className={`p-3 rounded-lg border-2 mb-3 ${
1111 isGroupOnly
1112 ? "bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800"
1113 : isPromoOnly
1114 ? "bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800"
1115 : "bg-purple-50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800"
1116 }`}
1117 >
1118 <div className="flex items-center gap-2">
1119 {isGroupOnly ? (
1120 <Users className="w-5 h-5 text-green-600 dark:text-green-400" />
1121 ) : isPromoOnly ? (
1122 <Info className="w-5 h-5 text-blue-600 dark:text-blue-400" />
1123 ) : (
1124 <Info className="w-5 h-5 text-purple-600 dark:text-purple-400" />
1125 )}
1126 <span
1127 className={`font-medium ${
1128 isGroupOnly
1129 ? "text-green-700 dark:text-green-300"
1130 : isPromoOnly
1131 ? "text-blue-700 dark:text-blue-300"
1132 : "text-purple-700 dark:text-purple-300"
1133 }`}
1134 >
1135 {isGroupOnly
1136 ? isEditMode
1137 ? __(
1138 "Group Discount (Auto-applies, no code needed)",
1139 "yatra",
1140 )
1141 : __(
1142 "Creating Group Discount (Auto-applies, no code needed)",
1143 "yatra",
1144 )
1145 : isPromoOnly
1146 ? isEditMode
1147 ? __("Promo Code Discount", "yatra")
1148 : __("Creating Promo Code Discount", "yatra")
1149 : isEditMode
1150 ? __("Promo Code + Group Discount", "yatra")
1151 : __(
1152 "Creating Promo Code + Group Discount",
1153 "yatra",
1154 )}
1155 </span>
1156 </div>
1157 </div>
1158 );
1159 })()}
1160
1161 {/* Basic Information */}
1162 <Card>
1163 <CardHeader className="pb-2">
1164 <CardTitle className="text-base">
1165 {isEffectiveGroupOnly
1166 ? __("Group Discount Information", "yatra")
1167 : __("Basic Information", "yatra")}
1168 </CardTitle>
1169 </CardHeader>
1170 <CardContent className="space-y-3">
1171 {/* Coupon Code - Only show for promo modes */}
1172 {!isEffectiveGroupOnly && (
1173 <div>
1174 <label
1175 htmlFor="code"
1176 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
1177 >
1178 {__("Coupon Code", "yatra")}{" "}
1179 <span className="text-red-500">*</span>
1180 </label>
1181 <HelpText
1182 text={__(
1183 "Enter a unique coupon code. Only uppercase letters, numbers, hyphens, and underscores are allowed.",
1184 "yatra",
1185 )}
1186 className="mb-2"
1187 />
1188 <Input
1189 id="code"
1190 name="code"
1191 type="text"
1192 value={formData.code}
1193 onChange={(e) =>
1194 handleFieldChange(
1195 "code",
1196 e.target.value.toUpperCase(),
1197 )
1198 }
1199 placeholder={__("e.g., SUMMER2024", "yatra")}
1200 className={errors.code ? "border-red-500" : ""}
1201 required
1202 />
1203 {errors.code && (
1204 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
1205 <Info className="w-4 h-4" />
1206 {errors.code}
1207 </p>
1208 )}
1209 </div>
1210 )}
1211
1212 {/* Internal Name for Group Discount Only */}
1213 {isEffectiveGroupOnly && (
1214 <div>
1215 <label
1216 htmlFor="code"
1217 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
1218 >
1219 {__("Internal Name", "yatra")}{" "}
1220 <span className="text-red-500">*</span>
1221 </label>
1222 <HelpText
1223 text={__(
1224 "Enter a unique internal name for this group discount. This is for your reference only and won't be shown to customers.",
1225 "Enter a unique internal name for this group discount. This is for your reference only and won't be shown to customers.",
1226 )}
1227 className="mb-2"
1228 />
1229 <Input
1230 id="code"
1231 name="code"
1232 type="text"
1233 value={formData.code}
1234 onChange={(e) =>
1235 handleFieldChange(
1236 "code",
1237 e.target.value.toUpperCase().replace(/\s+/g, "_"),
1238 )
1239 }
1240 placeholder={__("e.g., GROUP_DISCOUNT_2024", "yatra")}
1241 className={errors.code ? "border-red-500" : ""}
1242 required
1243 />
1244 {errors.code && (
1245 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
1246 <Info className="w-4 h-4" />
1247 {errors.code}
1248 </p>
1249 )}
1250 </div>
1251 )}
1252
1253 {/* Description */}
1254 <div>
1255 <label
1256 htmlFor="description"
1257 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
1258 >
1259 {__("Description", "yatra")}
1260 </label>
1261 <HelpText
1262 text={__(
1263 "Optional description for this discount coupon. This helps you identify the purpose of the coupon.",
1264 "yatra",
1265 )}
1266 className="mb-2"
1267 />
1268 <textarea
1269 id="description"
1270 value={formData.description}
1271 onChange={(e) =>
1272 handleFieldChange("description", e.target.value)
1273 }
1274 placeholder={__(
1275 "e.g., Summer discount for all trips",
1276 "yatra",
1277 )}
1278 rows={3}
1279 className="flex w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:ring-offset-gray-900 dark:placeholder:text-gray-400 dark:focus-visible:ring-blue-400 resize-none"
1280 />
1281 </div>
1282
1283 {/* Discount Type and Amount - Only show for promo modes */}
1284 {!isEffectiveGroupOnly && (
1285 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
1286 {/* Discount Type */}
1287 <div>
1288 <label
1289 htmlFor="type"
1290 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
1291 >
1292 {__("Discount Type", "yatra")}{" "}
1293 <span className="text-red-500">*</span>
1294 </label>
1295 <Select
1296 id="type"
1297 name="type"
1298 value={formData.type}
1299 onChange={(e) =>
1300 handleFieldChange(
1301 "type",
1302 e.target.value as "percentage" | "fixed",
1303 )
1304 }
1305 className={errors.type ? "border-red-500" : ""}
1306 required
1307 >
1308 <option value="percentage">
1309 {__("Percentage", "yatra")}
1310 </option>
1311 <option value="fixed">
1312 {__("Fixed Amount", "yatra")}
1313 </option>
1314 </Select>
1315 {errors.type && (
1316 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
1317 <Info className="w-4 h-4" />
1318 {errors.type}
1319 </p>
1320 )}
1321 </div>
1322
1323 {/* Discount Amount */}
1324 <div>
1325 <label
1326 htmlFor="amount"
1327 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
1328 >
1329 {__("Discount Amount", "yatra")}{" "}
1330 <span className="text-red-500">*</span>
1331 </label>
1332 <div className="relative">
1333 {formData.type === "percentage" ? (
1334 <Input
1335 id="amount"
1336 name="amount"
1337 type="number"
1338 min="0"
1339 max="100"
1340 step="0.01"
1341 value={formData.amount}
1342 onChange={(e) =>
1343 handleFieldChange("amount", e.target.value)
1344 }
1345 placeholder={__("e.g., 15", "yatra")}
1346 className={errors.amount ? "border-red-500" : ""}
1347 required
1348 />
1349 ) : (
1350 <div className="relative">
1351 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
1352 $
1353 </span>
1354 <Input
1355 id="amount"
1356 name="amount"
1357 type="number"
1358 min="0"
1359 step="0.01"
1360 value={formData.amount}
1361 onChange={(e) =>
1362 handleFieldChange("amount", e.target.value)
1363 }
1364 placeholder={__("e.g., 50", "yatra")}
1365 className={`pl-7 ${errors.amount ? "border-red-500" : ""}`}
1366 required
1367 />
1368 </div>
1369 )}
1370 {formData.type === "percentage" && (
1371 <span className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
1372 %
1373 </span>
1374 )}
1375 </div>
1376 {errors.amount && (
1377 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
1378 <Info className="w-4 h-4" />
1379 {errors.amount}
1380 </p>
1381 )}
1382 </div>
1383 </div>
1384 )}
1385
1386 {/* Maximum Discount Amount (for percentage discounts) - Only show for promo modes */}
1387 {!isEffectiveGroupOnly && formData.type === "percentage" && (
1388 <div>
1389 <label
1390 htmlFor="max_discount_amount"
1391 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
1392 >
1393 {__("Maximum Discount Amount", "yatra")}
1394 </label>
1395 <HelpText
1396 text={__(
1397 "Optional maximum discount cap. For example, if discount is 20% with max $500, a $5000 booking gets $500 off (not $1000).",
1398 "yatra",
1399 )}
1400 className="mb-2"
1401 />
1402 <div className="relative">
1403 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
1404 $
1405 </span>
1406 <Input
1407 id="max_discount_amount"
1408 type="number"
1409 min="0"
1410 step="0.01"
1411 value={formData.max_discount_amount}
1412 onChange={(e) =>
1413 handleFieldChange(
1414 "max_discount_amount",
1415 e.target.value,
1416 )
1417 }
1418 placeholder={__("e.g., 500 (optional)", "yatra")}
1419 className={`pl-7 ${errors.max_discount_amount ? "border-red-500" : ""}`}
1420 />
1421 </div>
1422 {errors.max_discount_amount && (
1423 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
1424 <Info className="w-4 h-4" />
1425 {errors.max_discount_amount}
1426 </p>
1427 )}
1428 </div>
1429 )}
1430 </CardContent>
1431 </Card>
1432
1433 {/* Usage & Restrictions */}
1434 <Card>
1435 <CardHeader className="pb-2">
1436 <CardTitle className="text-base">
1437 {__("Usage & Restrictions", "yatra")}
1438 </CardTitle>
1439 </CardHeader>
1440 <CardContent className="space-y-3">
1441 {/* Usage Limit (Total) */}
1442 <div>
1443 <label
1444 htmlFor="usage_limit"
1445 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
1446 >
1447 {__("Total Usage Limit", "yatra")}
1448 </label>
1449 <HelpText
1450 text={__(
1451 "Maximum number of times this coupon can be used across all customers. Enter 0 for unlimited usage.",
1452 "yatra",
1453 )}
1454 className="mb-2"
1455 />
1456 <Input
1457 id="usage_limit"
1458 type="number"
1459 min="0"
1460 value={formData.usage_limit}
1461 onChange={(e) =>
1462 handleFieldChange("usage_limit", e.target.value)
1463 }
1464 placeholder={__("0 for unlimited", "yatra")}
1465 className={errors.usage_limit ? "border-red-500" : ""}
1466 />
1467 {errors.usage_limit && (
1468 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
1469 <Info className="w-4 h-4" />
1470 {errors.usage_limit}
1471 </p>
1472 )}
1473 </div>
1474
1475 {/* Usage Limit Per Customer */}
1476 <div>
1477 <label
1478 htmlFor="usage_limit_per_customer"
1479 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
1480 >
1481 {__("Usage Limit Per Customer", "yatra")}
1482 </label>
1483 <HelpText
1484 text={__(
1485 "Maximum number of times a single customer can use this coupon. Enter 0 for unlimited usage per customer.",
1486 "yatra",
1487 )}
1488 className="mb-2"
1489 />
1490 <Input
1491 id="usage_limit_per_customer"
1492 type="number"
1493 min="0"
1494 value={formData.usage_limit_per_customer}
1495 onChange={(e) =>
1496 handleFieldChange(
1497 "usage_limit_per_customer",
1498 e.target.value,
1499 )
1500 }
1501 placeholder={__("0 for unlimited", "yatra")}
1502 className={
1503 errors.usage_limit_per_customer ? "border-red-500" : ""
1504 }
1505 />
1506 {errors.usage_limit_per_customer && (
1507 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
1508 <Info className="w-4 h-4" />
1509 {errors.usage_limit_per_customer}
1510 </p>
1511 )}
1512 </div>
1513
1514 {/* First-Time Customer Only */}
1515 <div>
1516 <label className="flex items-center gap-2 cursor-pointer">
1517 <input
1518 type="checkbox"
1519 checked={formData.first_time_customer_only}
1520 onChange={(e) =>
1521 handleFieldChange(
1522 "first_time_customer_only",
1523 e.target.checked,
1524 )
1525 }
1526 className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
1527 />
1528 <span className="text-sm font-medium text-gray-700 dark:text-gray-300">
1529 {__("First-Time Customers Only", "yatra")}
1530 </span>
1531 </label>
1532 <HelpText
1533 text={__(
1534 "If enabled, only customers who have never made a booking before can use this coupon.",
1535 "yatra",
1536 )}
1537 className="mb-2 mt-1"
1538 />
1539 </div>
1540
1541 {/* Minimum Amount */}
1542 <div>
1543 <label
1544 htmlFor="min_amount"
1545 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
1546 >
1547 {__("Minimum Booking Amount", "yatra")}
1548 </label>
1549 <HelpText
1550 text={__(
1551 "Minimum total booking amount required to use this coupon. This applies to the total booking value (trip price × number of travelers).",
1552 "yatra",
1553 )}
1554 className="mb-2"
1555 />
1556 <div className="relative">
1557 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
1558 $
1559 </span>
1560 <Input
1561 id="min_amount"
1562 type="number"
1563 min="0"
1564 step="0.01"
1565 value={formData.min_amount}
1566 onChange={(e) =>
1567 handleFieldChange("min_amount", e.target.value)
1568 }
1569 placeholder={__("e.g., 100", "yatra")}
1570 className={`pl-7 ${errors.min_amount ? "border-red-500" : ""}`}
1571 />
1572 </div>
1573 {errors.min_amount && (
1574 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
1575 <Info className="w-4 h-4" />
1576 {errors.min_amount}
1577 </p>
1578 )}
1579 </div>
1580
1581 {/* Valid From Date and Expiry Date - Same Line */}
1582 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
1583 {/* Valid From Date */}
1584 <div>
1585 <label
1586 htmlFor="valid_from"
1587 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
1588 >
1589 {__("Valid From", "yatra")}
1590 </label>
1591 <DatePicker
1592 value={formData.valid_from}
1593 onChange={(value) =>
1594 handleFieldChange("valid_from", value)
1595 }
1596 placeholder={__("Select start date", "yatra")}
1597 error={!!errors.valid_from}
1598 className={errors.valid_from ? "border-red-500" : ""}
1599 />
1600 {errors.valid_from && (
1601 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
1602 <Info className="w-4 h-4" />
1603 {errors.valid_from}
1604 </p>
1605 )}
1606 </div>
1607
1608 {/* Expiry Date */}
1609 <div>
1610 <label
1611 htmlFor="expiry_date"
1612 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
1613 >
1614 {__("Expiry Date", "yatra")}
1615 </label>
1616 <DatePicker
1617 value={formData.expiry_date}
1618 onChange={(value) =>
1619 handleFieldChange("expiry_date", value)
1620 }
1621 placeholder={__("Select expiry date", "yatra")}
1622 minDate={
1623 formData.valid_from
1624 ? new Date(formData.valid_from)
1625 : new Date()
1626 }
1627 error={!!errors.expiry_date}
1628 className={errors.expiry_date ? "border-red-500" : ""}
1629 />
1630 {errors.expiry_date && (
1631 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
1632 <Info className="w-4 h-4" />
1633 {errors.expiry_date}
1634 </p>
1635 )}
1636 </div>
1637 </div>
1638 </CardContent>
1639 </Card>
1640
1641 {/* Group Discount - Hide for promo-only mode */}
1642 {!isEffectivePromoOnly && (
1643 <Card>
1644 <CardHeader className="pb-2">
1645 <CardTitle className="text-base">
1646 {__("Group Discount Settings", "yatra")}
1647 </CardTitle>
1648 </CardHeader>
1649 <CardContent className="space-y-3">
1650 {/* Enable Group Discount - Hide checkbox for group-only mode (already enabled) */}
1651 {!isEffectiveGroupOnly && (
1652 <div>
1653 <label className="flex items-center gap-2 cursor-pointer">
1654 <input
1655 type="checkbox"
1656 checked={formData.is_group_discount}
1657 onChange={(e) =>
1658 handleGroupDiscountToggle(e.target.checked)
1659 }
1660 className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
1661 />
1662 <span className="text-sm font-medium text-gray-700 dark:text-gray-300">
1663 {__("Enable Group Discount", "yatra")}
1664 </span>
1665 </label>
1666 <HelpText
1667 text={__(
1668 "Apply discounts for group bookings based on traveler count.",
1669 "yatra",
1670 )}
1671 className="mb-2 mt-1"
1672 />
1673 </div>
1674 )}
1675
1676 {formData.is_group_discount && (
1677 <>
1678 {/* Group Discount Mode */}
1679 <div>
1680 <label
1681 htmlFor="group_discount_mode"
1682 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
1683 >
1684 {__("Discount Mode", "yatra")}{" "}
1685 <span className="text-red-500">*</span>
1686 </label>
1687 <HelpText
1688 text={__(
1689 "Choose how to apply the group discount: Total discount on entire booking, or category-based rates per traveler type.",
1690 "yatra",
1691 )}
1692 className="mb-2"
1693 />
1694 <Select
1695 id="group_discount_mode"
1696 value={formData.group_discount_mode}
1697 onChange={(e) =>
1698 handleFieldChange(
1699 "group_discount_mode",
1700 e.target.value as "total" | "category_based",
1701 )
1702 }
1703 className={
1704 errors.group_discount_mode ? "border-red-500" : ""
1705 }
1706 required={formData.is_group_discount}
1707 >
1708 <option value="total">
1709 {__("Total Discount", "yatra")}
1710 </option>
1711 <option value="category_based">
1712 {__("Category-Based Discount", "yatra")}
1713 </option>
1714 </Select>
1715 {errors.group_discount_mode && (
1716 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
1717 <Info className="w-4 h-4" />
1718 {errors.group_discount_mode}
1719 </p>
1720 )}
1721 </div>
1722
1723 {/* Ranges - Only show for Total Discount mode */}
1724 {formData.group_discount_mode === "total" && (
1725 <>
1726 <div className="flex items-center justify-between">
1727 <div>
1728 <h4 className="text-sm font-semibold text-gray-900 dark:text-white">
1729 {__("Group Size Ranges", "yatra")}
1730 </h4>
1731 <p className="text-xs text-gray-500 dark:text-gray-400">
1732 {__(
1733 "Add one or more ranges (e.g., 1-10, 10-12, 12-15, 15+). The first matching range will be applied.",
1734 "yatra",
1735 )}
1736 </p>
1737 </div>
1738 <Button
1739 type="button"
1740 variant="outline"
1741 onClick={addRange}
1742 className="flex items-center gap-2"
1743 >
1744 <Plus className="w-4 h-4" />
1745 {__("Add Range", "yatra")}
1746 </Button>
1747 </div>
1748
1749 <div className="space-y-3">
1750 {formData.group_discount_ranges.map(
1751 (range, idx) => (
1752 <Card
1753 key={range.id}
1754 className="border border-gray-200 dark:border-gray-700"
1755 >
1756 <CardContent className="p-4 space-y-3">
1757 <div className="flex items-start justify-between gap-3">
1758 <div className="text-sm font-semibold text-gray-900 dark:text-white">
1759 {__("Range", "yatra")} #{idx + 1}
1760 </div>
1761 <Button
1762 type="button"
1763 variant="outline"
1764 onClick={() => removeRange(range.id)}
1765 disabled={
1766 formData.group_discount_ranges
1767 .length === 1
1768 }
1769 className="flex items-center gap-2"
1770 >
1771 <Trash2 className="w-4 h-4" />
1772 {__("Remove", "yatra")}
1773 </Button>
1774 </div>
1775
1776 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
1777 <div>
1778 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1779 {__("Minimum Group Size", "yatra")}{" "}
1780 <span className="text-red-500">
1781 *
1782 </span>
1783 </label>
1784 <Input
1785 type="number"
1786 min="1"
1787 value={range.min_group_size}
1788 onChange={(e) =>
1789 updateRange(range.id, {
1790 min_group_size: e.target.value,
1791 })
1792 }
1793 placeholder={__(
1794 "e.g., 10",
1795 "yatra",
1796 )}
1797 className={
1798 errors[
1799 `group_discount_ranges_${idx}_min`
1800 ]
1801 ? "border-red-500"
1802 : ""
1803 }
1804 />
1805 {errors[
1806 `group_discount_ranges_${idx}_min`
1807 ] && (
1808 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
1809 <Info className="w-4 h-4" />
1810 {
1811 errors[
1812 `group_discount_ranges_${idx}_min`
1813 ]
1814 }
1815 </p>
1816 )}
1817 </div>
1818
1819 <div>
1820 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1821 {__("Maximum Group Size", "yatra")}
1822 </label>
1823 <Input
1824 type="number"
1825 min={
1826 range.min_group_size
1827 ? parseInt(
1828 range.min_group_size,
1829 10,
1830 ) + 1
1831 : 2
1832 }
1833 value={range.max_group_size}
1834 onChange={(e) =>
1835 updateRange(range.id, {
1836 max_group_size: e.target.value,
1837 })
1838 }
1839 placeholder={__(
1840 "Leave empty for unlimited",
1841 "yatra",
1842 )}
1843 className={
1844 errors[
1845 `group_discount_ranges_${idx}_max`
1846 ]
1847 ? "border-red-500"
1848 : ""
1849 }
1850 />
1851 {errors[
1852 `group_discount_ranges_${idx}_max`
1853 ] && (
1854 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
1855 <Info className="w-4 h-4" />
1856 {
1857 errors[
1858 `group_discount_ranges_${idx}_max`
1859 ]
1860 }
1861 </p>
1862 )}
1863 </div>
1864 </div>
1865
1866 {formData.group_discount_mode ===
1867 "total" && (
1868 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
1869 <div>
1870 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1871 {__("Discount Type", "yatra")}{" "}
1872 <span className="text-red-500">
1873 *
1874 </span>
1875 </label>
1876 <Select
1877 value={
1878 range.discount_type ||
1879 "percentage"
1880 }
1881 onChange={(e) =>
1882 updateRange(range.id, {
1883 discount_type: e.target
1884 .value as
1885 | "percentage"
1886 | "fixed",
1887 })
1888 }
1889 className={
1890 errors[
1891 `group_discount_ranges_${idx}_type`
1892 ]
1893 ? "border-red-500"
1894 : ""
1895 }
1896 >
1897 <option value="percentage">
1898 {__("Percentage", "yatra")}
1899 </option>
1900 <option value="fixed">
1901 {__("Fixed Amount", "yatra")}
1902 </option>
1903 <option value="percentage_of_total">
1904 {__(
1905 "Percentage of Total",
1906 "yatra",
1907 )}
1908 </option>
1909 </Select>
1910 {errors[
1911 `group_discount_ranges_${idx}_type`
1912 ] && (
1913 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
1914 <Info className="w-4 h-4" />
1915 {
1916 errors[
1917 `group_discount_ranges_${idx}_type`
1918 ]
1919 }
1920 </p>
1921 )}
1922 </div>
1923
1924 <div>
1925 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
1926 {__("Discount Amount", "yatra")}{" "}
1927 <span className="text-red-500">
1928 *
1929 </span>
1930 </label>
1931 <div className="relative">
1932 {range.discount_type ===
1933 "fixed" && (
1934 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
1935 $
1936 </span>
1937 )}
1938 <Input
1939 type="number"
1940 min="0"
1941 max={
1942 range.discount_type ===
1943 "percentage"
1944 ? 100
1945 : undefined
1946 }
1947 step="0.01"
1948 value={
1949 range.discount_amount || ""
1950 }
1951 onChange={(e) =>
1952 updateRange(range.id, {
1953 discount_amount:
1954 e.target.value,
1955 })
1956 }
1957 placeholder={
1958 range.discount_type ===
1959 "percentage"
1960 ? __("e.g., 10", "yatra")
1961 : __("e.g., 50", "yatra")
1962 }
1963 className={`${range.discount_type === "fixed" ? "pl-7" : ""} ${errors[`group_discount_ranges_${idx}_amount`] ? "border-red-500" : ""}`}
1964 />
1965 {range.discount_type ===
1966 "percentage" && (
1967 <span className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
1968 %
1969 </span>
1970 )}
1971 </div>
1972 {errors[
1973 `group_discount_ranges_${idx}_amount`
1974 ] && (
1975 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
1976 <Info className="w-4 h-4" />
1977 {
1978 errors[
1979 `group_discount_ranges_${idx}_amount`
1980 ]
1981 }
1982 </p>
1983 )}
1984 </div>
1985 </div>
1986 )}
1987 </CardContent>
1988 </Card>
1989 ),
1990 )}
1991 </div>
1992 </>
1993 )}
1994
1995 {formData.group_discount_mode === "category_based" && (
1996 <div className="space-y-4">
1997 <div className="flex items-center justify-between">
1998 <div>
1999 <h4 className="text-sm font-semibold text-gray-900 dark:text-white">
2000 {__("Traveler Categories", "yatra")}
2001 </h4>
2002 <p className="text-xs text-gray-500 dark:text-gray-400">
2003 {__(
2004 "Add traveler categories and define discount ranges for each category.",
2005 "yatra",
2006 )}
2007 </p>
2008 </div>
2009 <div className="relative">
2010 <Button
2011 type="button"
2012 onClick={addTravelerCategory}
2013 className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
2014 >
2015 <Plus className="w-4 h-4" />
2016 {__("Add Category", "yatra")}
2017 </Button>
2018
2019 {/* Category Selection Dropdown */}
2020 {showCategoryDropdown && (
2021 <div className="absolute right-0 top-full mt-2 z-50 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 shadow-lg min-w-[320px]">
2022 <div className="p-3 border-b border-gray-200 dark:border-gray-700">
2023 <p className="text-sm text-gray-600 dark:text-gray-400">
2024 {__(
2025 "Select a category to add pricing",
2026 "yatra",
2027 )}
2028 </p>
2029 </div>
2030
2031 {travelerCategoryOptions.length > 0 ? (
2032 <div className="divide-y divide-gray-100 dark:divide-gray-700 max-h-[300px] overflow-y-auto">
2033 {travelerCategoryOptions.map((cat) => (
2034 <div
2035 key={cat.value}
2036 onClick={() =>
2037 selectCategory(
2038 cat.value,
2039 cat.label,
2040 )
2041 }
2042 className="px-4 py-3 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
2043 >
2044 <div className="flex flex-col">
2045 <div className="flex items-center gap-2">
2046 <span className="font-semibold text-gray-900 dark:text-white">
2047 {cat.label}
2048 </span>
2049 <span className="text-sm text-gray-500 dark:text-gray-400">
2050 ({cat.age_min || "null"}-
2051 {cat.age_max || "null"}{" "}
2052 {__("years", "yatra")})
2053 </span>
2054 </div>
2055 <div className="text-sm text-gray-500 dark:text-gray-400">
2056 {cat.description && (
2057 <span>
2058 {cat.description} •{" "}
2059 </span>
2060 )}
2061 <span className="capitalize">
2062 {cat.pricing_mode?.replace(
2063 "_",
2064 " ",
2065 ) || "Per person"}
2066 </span>
2067 </div>
2068 </div>
2069 </div>
2070 ))}
2071 </div>
2072 ) : (
2073 <div className="p-4 text-center text-sm text-gray-500 dark:text-gray-400">
2074 {__(
2075 "No traveler categories available. Please create categories first.",
2076 "yatra",
2077 )}
2078 </div>
2079 )}
2080 </div>
2081 )}
2082 </div>
2083 </div>
2084
2085 {errors.category_discounts && (
2086 <p className="text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
2087 <Info className="w-4 h-4" />
2088 {errors.category_discounts}
2089 </p>
2090 )}
2091
2092 {formData.category_discounts.length === 0 ? (
2093 <div className="text-center py-12 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg bg-gray-50 dark:bg-gray-800/50">
2094 <div className="flex flex-col items-center space-y-4">
2095 <div className="w-16 h-16 bg-gray-200 dark:bg-gray-700 rounded-full flex items-center justify-center">
2096 <Users className="w-8 h-8 text-gray-500 dark:text-gray-400" />
2097 </div>
2098 <div className="text-gray-500 dark:text-gray-400">
2099 <div className="text-lg font-medium mb-2">
2100 {__(
2101 "No traveler categories added yet",
2102 "yatra",
2103 )}
2104 </div>
2105 <div className="text-sm mb-4">
2106 {__(
2107 "Select traveler categories to apply group-specific discounts",
2108 "yatra",
2109 )}
2110 </div>
2111 </div>
2112 <Button
2113 type="button"
2114 onClick={addTravelerCategory}
2115 className="flex items-center gap-2 px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
2116 >
2117 <Plus className="w-4 h-4" />
2118 {__("Add Your First Category", "yatra")}
2119 </Button>
2120 </div>
2121 </div>
2122 ) : (
2123 <div className="space-y-4">
2124 {formData.category_discounts.map(
2125 (category, catIdx) => (
2126 <Card
2127 key={catIdx}
2128 className="border border-gray-200 dark:border-gray-700"
2129 >
2130 <CardContent className="p-4 space-y-3">
2131 <div className="flex items-start justify-between gap-3">
2132 <div className="flex-1">
2133 <div className="text-sm font-semibold text-gray-900 dark:text-white">
2134 {__("Traveler Category", "yatra")}{" "}
2135 #{catIdx + 1}
2136 </div>
2137 <div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
2138 {__(
2139 "Configure discount ranges for this traveler category.",
2140 "yatra",
2141 )}
2142 </div>
2143 </div>
2144 <Button
2145 type="button"
2146 variant="outline"
2147 onClick={() =>
2148 removeTravelerCategory(catIdx)
2149 }
2150 disabled={
2151 formData.category_discounts
2152 .length === 1
2153 }
2154 className="flex items-center gap-2"
2155 >
2156 <Trash2 className="w-4 h-4" />
2157 {__("Remove", "yatra")}
2158 </Button>
2159 </div>
2160
2161 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
2162 <div>
2163 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
2164 {__(
2165 "Select Traveler Category",
2166 "yatra",
2167 )}{" "}
2168 <span className="text-red-500">
2169 *
2170 </span>
2171 </label>
2172 <Select
2173 value={
2174 category.traveler_category_id
2175 }
2176 onChange={(e) => {
2177 const selected =
2178 travelerCategoryOptions.find(
2179 (opt) =>
2180 opt.value ===
2181 e.target.value,
2182 );
2183 updateTravelerCategory(catIdx, {
2184 traveler_category_id:
2185 e.target.value,
2186 traveler_category_label:
2187 selected?.label || "",
2188 });
2189 }}
2190 className={
2191 errors[
2192 `category_discounts_${catIdx}_id`
2193 ]
2194 ? "border-red-500"
2195 : ""
2196 }
2197 >
2198 {travelerCategoryOptions.map(
2199 (opt) => (
2200 <option
2201 key={opt.value}
2202 value={opt.value}
2203 >
2204 {opt.label}
2205 </option>
2206 ),
2207 )}
2208 </Select>
2209 {errors[
2210 `category_discounts_${catIdx}_id`
2211 ] && (
2212 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
2213 <Info className="w-4 h-4" />
2214 {
2215 errors[
2216 `category_discounts_${catIdx}_id`
2217 ]
2218 }
2219 </p>
2220 )}
2221 </div>
2222 </div>
2223
2224 <div className="flex items-center justify-between">
2225 <div>
2226 <div className="text-sm font-medium text-gray-900 dark:text-white">
2227 {__("Discount Ranges", "yatra")}
2228 </div>
2229 <div className="text-xs text-gray-500 dark:text-gray-400">
2230 {__(
2231 "Add multiple group size ranges for this category (e.g., 1-10, 10-12, 12-15, 15+).",
2232 "yatra",
2233 )}
2234 </div>
2235 </div>
2236 <Button
2237 type="button"
2238 variant="outline"
2239 onClick={() =>
2240 addRangeToCategory(catIdx)
2241 }
2242 className="flex items-center gap-2"
2243 >
2244 <Plus className="w-4 h-4" />
2245 {__("Add Range", "yatra")}
2246 </Button>
2247 </div>
2248
2249 {errors[
2250 `category_discounts_${catIdx}_ranges`
2251 ] && (
2252 <p className="text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
2253 <Info className="w-4 h-4" />
2254 {
2255 errors[
2256 `category_discounts_${catIdx}_ranges`
2257 ]
2258 }
2259 </p>
2260 )}
2261
2262 {category.ranges.length === 0 ? (
2263 <div className="text-center py-6 border-2 border-dashed border-gray-200 dark:border-gray-600 rounded-lg">
2264 <div className="text-gray-500 dark:text-gray-400">
2265 <div className="text-sm font-medium mb-1">
2266 {__(
2267 "No discount ranges added yet",
2268 "yatra",
2269 )}
2270 </div>
2271 <div className="text-xs">
2272 {__(
2273 'Click "Add Range" to create discount tiers',
2274 "yatra",
2275 )}
2276 </div>
2277 </div>
2278 </div>
2279 ) : (
2280 <div className="space-y-3">
2281 {category.ranges.map(
2282 (range, rIdx) => (
2283 <div
2284 key={range.id}
2285 className="border border-gray-200 dark:border-gray-600 rounded-lg p-4 space-y-3 bg-gray-50 dark:bg-gray-800"
2286 >
2287 <div className="flex items-start justify-between gap-3">
2288 <div className="text-sm font-medium text-gray-900 dark:text-white">
2289 {__("Range", "yatra")} #
2290 {rIdx + 1}
2291 </div>
2292 <Button
2293 type="button"
2294 variant="outline"
2295 size="sm"
2296 onClick={() =>
2297 removeRangeFromCategory(
2298 catIdx,
2299 range.id,
2300 )
2301 }
2302 disabled={
2303 category.ranges
2304 .length === 1
2305 }
2306 className="flex items-center gap-1"
2307 >
2308 <Trash2 className="w-3 h-3" />
2309 {__("Remove", "yatra")}
2310 </Button>
2311 </div>
2312
2313 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
2314 <div>
2315 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
2316 {__(
2317 "Minimum Group Size",
2318 "yatra",
2319 )}{" "}
2320 <span className="text-red-500">
2321 *
2322 </span>
2323 </label>
2324 <Input
2325 type="number"
2326 min="1"
2327 value={
2328 range.min_group_size
2329 }
2330 onChange={(e) =>
2331 updateRangeInCategory(
2332 catIdx,
2333 range.id,
2334 {
2335 min_group_size:
2336 e.target.value,
2337 },
2338 )
2339 }
2340 placeholder={__(
2341 "e.g., 10",
2342 "yatra",
2343 )}
2344 className={
2345 errors[
2346 `category_discounts_${catIdx}_ranges_${rIdx}_min`
2347 ]
2348 ? "border-red-500"
2349 : ""
2350 }
2351 />
2352 {errors[
2353 `category_discounts_${catIdx}_ranges_${rIdx}_min`
2354 ] && (
2355 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
2356 <Info className="w-4 h-4" />
2357 {
2358 errors[
2359 `category_discounts_${catIdx}_ranges_${rIdx}_min`
2360 ]
2361 }
2362 </p>
2363 )}
2364 </div>
2365
2366 <div>
2367 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
2368 {__(
2369 "Maximum Group Size",
2370 "yatra",
2371 )}
2372 </label>
2373 <Input
2374 type="number"
2375 min={
2376 range.min_group_size
2377 ? parseInt(
2378 range.min_group_size,
2379 10,
2380 ) + 1
2381 : 2
2382 }
2383 value={
2384 range.max_group_size
2385 }
2386 onChange={(e) =>
2387 updateRangeInCategory(
2388 catIdx,
2389 range.id,
2390 {
2391 max_group_size:
2392 e.target.value,
2393 },
2394 )
2395 }
2396 placeholder={__(
2397 "Leave empty for unlimited",
2398 "yatra",
2399 )}
2400 className={
2401 errors[
2402 `category_discounts_${catIdx}_ranges_${rIdx}_max`
2403 ]
2404 ? "border-red-500"
2405 : ""
2406 }
2407 />
2408 {errors[
2409 `category_discounts_${catIdx}_ranges_${rIdx}_max`
2410 ] && (
2411 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
2412 <Info className="w-4 h-4" />
2413 {
2414 errors[
2415 `category_discounts_${catIdx}_ranges_${rIdx}_max`
2416 ]
2417 }
2418 </p>
2419 )}
2420 </div>
2421 </div>
2422
2423 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
2424 <div>
2425 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
2426 {__(
2427 "Discount Type",
2428 "yatra",
2429 )}{" "}
2430 <span className="text-red-500">
2431 *
2432 </span>
2433 </label>
2434 <Select
2435 value={
2436 range.discount_type ||
2437 "percentage"
2438 }
2439 onChange={(e) =>
2440 updateRangeInCategory(
2441 catIdx,
2442 range.id,
2443 {
2444 discount_type: e
2445 .target
2446 .value as
2447 | "percentage"
2448 | "fixed",
2449 },
2450 )
2451 }
2452 className={
2453 errors[
2454 `category_discounts_${catIdx}_ranges_${rIdx}_type`
2455 ]
2456 ? "border-red-500"
2457 : ""
2458 }
2459 >
2460 <option value="percentage">
2461 {__(
2462 "Percentage",
2463 "yatra",
2464 )}
2465 </option>
2466 <option value="fixed">
2467 {__(
2468 "Fixed Amount",
2469 "yatra",
2470 )}
2471 </option>
2472 </Select>
2473 {errors[
2474 `category_discounts_${catIdx}_ranges_${rIdx}_type`
2475 ] && (
2476 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
2477 <Info className="w-4 h-4" />
2478 {
2479 errors[
2480 `category_discounts_${catIdx}_ranges_${rIdx}_type`
2481 ]
2482 }
2483 </p>
2484 )}
2485 </div>
2486
2487 <div>
2488 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
2489 {__(
2490 "Discount Amount",
2491 "yatra",
2492 )}{" "}
2493 <span className="text-red-500">
2494 *
2495 </span>
2496 </label>
2497 <div className="relative">
2498 {range.discount_type ===
2499 "fixed" && (
2500 <span className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
2501 $
2502 </span>
2503 )}
2504 <Input
2505 type="number"
2506 min="0"
2507 max={
2508 range.discount_type ===
2509 "percentage"
2510 ? 100
2511 : undefined
2512 }
2513 step="0.01"
2514 value={
2515 range.discount_amount ||
2516 ""
2517 }
2518 onChange={(e) =>
2519 updateRangeInCategory(
2520 catIdx,
2521 range.id,
2522 {
2523 discount_amount:
2524 e.target
2525 .value,
2526 },
2527 )
2528 }
2529 placeholder={
2530 range.discount_type ===
2531 "percentage"
2532 ? __(
2533 "e.g., 10",
2534 "yatra",
2535 )
2536 : __(
2537 "e.g., 50",
2538 "yatra",
2539 )
2540 }
2541 className={`${range.discount_type === "fixed" ? "pl-7" : ""} ${errors[`category_discounts_${catIdx}_ranges_${rIdx}_amount`] ? "border-red-500" : ""}`}
2542 />
2543 {range.discount_type ===
2544 "percentage" && (
2545 <span className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 text-sm">
2546 %
2547 </span>
2548 )}
2549 </div>
2550 {errors[
2551 `category_discounts_${catIdx}_ranges_${rIdx}_amount`
2552 ] && (
2553 <p className="mt-1.5 text-sm text-red-600 dark:text-red-400 flex items-center gap-1">
2554 <Info className="w-4 h-4" />
2555 {
2556 errors[
2557 `category_discounts_${catIdx}_ranges_${rIdx}_amount`
2558 ]
2559 }
2560 </p>
2561 )}
2562 </div>
2563 </div>
2564 </div>
2565 ),
2566 )}
2567 </div>
2568 )}
2569 </CardContent>
2570 </Card>
2571 ),
2572 )}
2573 </div>
2574 )}
2575 </div>
2576 )}
2577 </>
2578 )}
2579 </CardContent>
2580 </Card>
2581 )}
2582 </div>
2583
2584 {/* Sidebar */}
2585 <div className="space-y-3">
2586 {/* Status */}
2587 <Card>
2588 <CardHeader className="pb-2">
2589 <CardTitle className="text-base">
2590 {__("Status", "yatra")}
2591 </CardTitle>
2592 </CardHeader>
2593 <CardContent className="space-y-3">
2594 <Select
2595 value={formData.status}
2596 onChange={(e) =>
2597 handleFieldChange(
2598 "status",
2599 e.target.value as "draft" | "publish" | "trash",
2600 )
2601 }
2602 >
2603 <option value="draft">{__("Draft", "yatra")}</option>
2604 <option value="publish">{__("Publish", "yatra")}</option>
2605 <option value="trash">{__("Trash", "yatra")}</option>
2606 </Select>
2607 <p className="text-xs text-gray-500 dark:text-gray-400">
2608 {__(
2609 "Only published coupons can be used by customers.",
2610 "yatra",
2611 )}
2612 </p>
2613 </CardContent>
2614 </Card>
2615
2616 {/* Applicable To */}
2617 <Card>
2618 <CardHeader className="pb-2">
2619 <CardTitle className="text-base">
2620 {__("Applicable To", "yatra")}
2621 </CardTitle>
2622 </CardHeader>
2623 <CardContent>
2624 <ApplicableTripSelector
2625 value={formData.applicable_to}
2626 onValueChange={(val) =>
2627 handleFieldChange("applicable_to", val)
2628 }
2629 selectedTripIds={formData.trip_ids}
2630 onTripIdsChange={(ids) =>
2631 handleFieldChange("trip_ids", ids)
2632 }
2633 description={__(
2634 "Choose whether this discount applies to all trips or specific ones.",
2635 )}
2636 helperText={__(
2637 "Trips shown with ID for quick identification.",
2638 )}
2639 />
2640 </CardContent>
2641 </Card>
2642
2643 {/* Submit Actions */}
2644 <Card>
2645 <CardContent className="p-3">
2646 <div className="space-y-2">
2647 {errors.submit && (
2648 <div className="p-2 text-sm text-red-500 bg-red-50 dark:bg-red-900/20 rounded-md">
2649 {errors.submit}
2650 </div>
2651 )}
2652 <div className="flex gap-2">
2653 <Button
2654 type="submit"
2655 disabled={isSubmitting}
2656 className="flex-1 flex items-center justify-center gap-2"
2657 >
2658 {isSubmitting ? (
2659 <>
2660 <Loader2 className="w-4 h-4 animate-spin" />
2661 {__("Saving...", "yatra")}
2662 </>
2663 ) : (
2664 <>
2665 <Save className="w-4 h-4" />
2666 {isEditMode
2667 ? __("Update Coupon", "yatra")
2668 : __("Create Coupon", "yatra")}
2669 </>
2670 )}
2671 </Button>
2672 <Button
2673 type="button"
2674 variant="outline"
2675 onClick={handleBack}
2676 disabled={isSubmitting}
2677 >
2678 {__("Cancel", "yatra")}
2679 </Button>
2680 </div>
2681 </div>
2682 </CardContent>
2683 </Card>
2684 </div>
2685 </div>
2686 </form>
2687 </ConditionalRender>
2688
2689 {/* Premium Upgrade Modal */}
2690 <PremiumUpgradeDialog
2691 open={showPremiumModal}
2692 onClose={() => setShowPremiumModal(false)}
2693 moduleName={__("Group Discounts", "yatra")}
2694 purchaseUrl="https://wpyatra.com/pricing?module=group-discounts"
2695 moduleDescription={__(
2696 "Unlock powerful group discount features to automatically apply discounts based on group size and traveler categories.",
2697 "yatra",
2698 )}
2699 />
2700 </div>
2701 );
2702 };
2703
2704 export default DiscountForm;
2705