PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.9
Yatra – Travel Booking & Tour Operator Software v3.0.9
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.9, at resources/js/pages/DiscountForm.tsx

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