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 / components / availability / RecurringRules.tsx

RecurringRules.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.5, at resources/js/components/availability/RecurringRules.tsx

917 lines 28.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Recurring Availability Rules Component
3 * Manage recurring patterns for trip availability
4 * Uses the same UI structure as Specific Dates table
5 */
6
7 import React, { useState, useMemo } from "react";
8 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
9 import {
10 Plus,
11 Edit,
12 Trash2,
13 Copy,
14 CheckCircle,
15 XCircle,
16 AlertCircle,
17 RefreshCw,
18 Search,
19 X,
20 } from "lucide-react";
21 import { __ } from "../../lib/i18n";
22 import { Button } from "../ui/button";
23 import { Input } from "../ui/input";
24 import { Select } from "../ui/select";
25 import { Card, CardContent } from "../ui/card";
26 import { Badge } from "../ui/badge";
27 import { apiClient } from "../../lib/api-client";
28 import { useToast } from "../ui/toast";
29 import { BulkActionToolbar, Table as SharedTable } from "../shared";
30 import { ConfirmationDialog } from "../ui/confirmation-dialog";
31 import { formatYatraMoney } from "../../lib/currency-display";
32
33 interface RecurringRule {
34 id: number;
35 trip_id: number;
36 name: string;
37 rule_type: "weekly" | "monthly" | "interval";
38 days_of_week?: string;
39 days_of_week_array?: number[];
40 week_of_month?: "first" | "second" | "third" | "fourth" | "last";
41 day_of_week?: number;
42 interval_days?: number;
43 start_date: string;
44 end_date?: string;
45 excluded_dates: string[];
46 original_price?: number;
47 sale_price?: number;
48 seats_total: number;
49 departure_time?: string;
50 arrival_time?: string;
51 from_location?: string;
52 to_location?: string;
53 cutoff_hours: number;
54 status: "active" | "inactive";
55 generated_count?: number;
56 pricing_type?: "regular" | "traveler_based";
57 traveler_pricing?: Array<{
58 category_id: number;
59 category_name?: string;
60 original_price: number;
61 sale_price?: number;
62 }>;
63 }
64
65 interface RecurringRulesProps {
66 tripId: number;
67 tripName?: string;
68 tripType?: "single_day" | "multi_day";
69 pricingType?: "regular" | "traveler_based";
70 onAddRule: () => void;
71 onEditRule: (id: number) => void;
72 }
73
74 const dayNames = [
75 { value: 0, label: "Sunday" },
76 { value: 1, label: "Monday" },
77 { value: 2, label: "Tuesday" },
78 { value: 3, label: "Wednesday" },
79 { value: 4, label: "Thursday" },
80 { value: 5, label: "Friday" },
81 { value: 6, label: "Saturday" },
82 ];
83
84 export const RecurringRules: React.FC<RecurringRulesProps> = ({
85 tripId,
86 tripName,
87 tripType = "multi_day",
88 pricingType = "regular",
89 onAddRule,
90 onEditRule,
91 }) => {
92 const isSingleDayTrip = tripType === "single_day";
93 const isTravelerBased = pricingType === "traveler_based";
94 const queryClient = useQueryClient();
95 const { showToast } = useToast();
96 const adminCurrency =
97 (typeof window !== "undefined" &&
98 (window as unknown as { yatraAdmin?: { currency?: string } }).yatraAdmin
99 ?.currency) ||
100 "USD";
101
102 // State management
103 const [searchTerm, setSearchTerm] = useState("");
104 const [statusFilter, setStatusFilter] = useState("all");
105 const [selectedIds, setSelectedIds] = useState<(string | number)[]>([]);
106 const [bulkAction, setBulkAction] = useState("");
107 const [showColumnsDropdown, setShowColumnsDropdown] = useState(false);
108 const [visibleColumns, setVisibleColumns] = useState({
109 name: true,
110 pattern: true,
111 start_date: true,
112 end_date: true,
113 capacity: true,
114 generated: true,
115 price: true,
116 status: true,
117 });
118
119 // Confirmation dialogs
120 const [deleteConfirm, setDeleteConfirm] = useState<{
121 isOpen: boolean;
122 rule: RecurringRule | null;
123 }>({
124 isOpen: false,
125 rule: null,
126 });
127 const [duplicateConfirm, setDuplicateConfirm] = useState<{
128 isOpen: boolean;
129 rule: RecurringRule | null;
130 }>({
131 isOpen: false,
132 rule: null,
133 });
134
135 // Format rule pattern for display
136 const formatRulePattern = (rule: RecurringRule): string => {
137 switch (rule.rule_type) {
138 case "weekly":
139 const days = (rule.days_of_week_array || [])
140 .map((d) => dayNames.find((dn) => dn.value === d)?.label.slice(0, 3))
141 .filter(Boolean)
142 .join(", ");
143 return `Every ${days}`;
144 case "monthly":
145 return `${rule.week_of_month || ""} ${dayNames.find((d) => d.value === rule.day_of_week)?.label || ""} of month`;
146 case "interval":
147 return `Every ${rule.interval_days} days`;
148 default:
149 return "Unknown pattern";
150 }
151 };
152
153 // Fetch status counts from API endpoint.
154 // Key is nested under ["recurring-availability", ...] so a single
155 // invalidateQueries({ queryKey: ["recurring-availability"] }) refreshes
156 // both the list AND the status badge counts (All / Active / Inactive).
157 // Otherwise, after deleting the last rule, the list correctly went to 0
158 // while the badges remained stale at 1, which looked like "no data but
159 // 1 on All and Active" for the trip.
160 const { data: countsData } = useQuery({
161 queryKey: ["recurring-availability", "counts", tripId],
162 queryFn: async () => {
163 const response = await apiClient.get("/recurring-availability/counts", {
164 params: {
165 trip_id: tripId,
166 },
167 });
168 return response || { all: 0, active: 0, inactive: 0 };
169 },
170 enabled: !!tripId,
171 staleTime: 0,
172 gcTime: 0,
173 });
174
175 const statusCounts = countsData || { all: 0, active: 0, inactive: 0 };
176
177 // Fetch recurring rules (no caching, always fresh data)
178 const { data: rulesData, isLoading } = useQuery({
179 queryKey: ["recurring-availability", tripId],
180 queryFn: async () => {
181 const response = await apiClient.get("/recurring-availability", {
182 params: {
183 trip_id: tripId,
184 },
185 });
186 return {
187 rules: (response?.data || []) as RecurringRule[],
188 total: response?.total || 0,
189 };
190 },
191 enabled: !!tripId,
192 staleTime: 0, // Always fetch fresh data
193 gcTime: 0, // Don't cache the data (replaces cacheTime in newer versions)
194 });
195
196 // eslint-disable-next-line react-hooks/exhaustive-deps
197 const allRules = rulesData?.rules || [];
198
199 // Filter rules based on status and search
200 const rules = useMemo(() => {
201 let filtered = allRules;
202
203 // Filter by status
204 if (statusFilter !== "all") {
205 filtered = filtered.filter(
206 (r: RecurringRule) => r.status === statusFilter,
207 );
208 }
209
210 // Filter by search term
211 if (searchTerm) {
212 const search = searchTerm.toLowerCase();
213 filtered = filtered.filter(
214 (r: RecurringRule) =>
215 (r.name && r.name.toLowerCase().includes(search)) ||
216 formatRulePattern(r).toLowerCase().includes(search),
217 );
218 }
219
220 return filtered;
221 }, [allRules, statusFilter, searchTerm]);
222
223 // Delete mutation
224 const deleteMutation = useMutation({
225 mutationFn: async (id: number) => {
226 return await apiClient.delete(`/recurring-availability/${id}`);
227 },
228 onSuccess: () => {
229 // Invalidate list AND badge counts. Both keys share the
230 // ["recurring-availability", ...] prefix so a single call refreshes
231 // both, but we keep the list key around in case a future refactor
232 // narrows the prefix.
233 queryClient.invalidateQueries({ queryKey: ["recurring-availability"] });
234 showToast(__("Recurring rule deleted successfully", "yatra"), "success");
235 setDeleteConfirm({ isOpen: false, rule: null });
236 setSelectedIds([]);
237 },
238 onError: (error: any) => {
239 showToast(
240 error?.message || __("Failed to delete rule", "yatra"),
241 "error",
242 );
243 },
244 });
245
246 // Bulk delete mutation
247 const bulkDeleteMutation = useMutation({
248 mutationFn: async (ids: string[]) => {
249 await Promise.all(
250 ids.map((id) => apiClient.delete(`/recurring-availability/${id}`)),
251 );
252 },
253 onSuccess: () => {
254 queryClient.invalidateQueries({ queryKey: ["recurring-availability"] });
255 showToast(__("Rules deleted successfully", "yatra"), "success");
256 setSelectedIds([]);
257 },
258 onError: (error: any) => {
259 showToast(
260 error?.message || __("Failed to delete rules", "yatra"),
261 "error",
262 );
263 },
264 });
265
266 // Duplicate rule mutation
267 const duplicateMutation = useMutation({
268 mutationFn: async (id: number) => {
269 const response = await apiClient.post(
270 `/recurring-availability/${id}/duplicate`,
271 );
272 return response;
273 },
274 onSuccess: () => {
275 queryClient.invalidateQueries({ queryKey: ["recurring-availability"] });
276 showToast(__("Rule duplicated successfully", "yatra"), "success");
277 setDuplicateConfirm({ isOpen: false, rule: null });
278 },
279 onError: (error: any) => {
280 showToast(
281 error?.message || __("Failed to duplicate rule", "yatra"),
282 "error",
283 );
284 },
285 });
286
287 // Handle bulk actions
288 const handleBulkApply = () => {
289 if (!bulkAction || selectedIds.length === 0) {
290 showToast(__("Please select rules and an action", "yatra"), "warning");
291 return;
292 }
293
294 switch (bulkAction) {
295 case "delete":
296 if (
297 confirm(
298 __(
299 "Are you sure you want to delete {count} rule(s)?",
300 "yatra",
301 ).replace("{count}", selectedIds.length.toString()),
302 )
303 ) {
304 bulkDeleteMutation.mutate(selectedIds.map((id) => id.toString()));
305 }
306 break;
307 }
308
309 setBulkAction("");
310 };
311
312 // Toggle column visibility
313 const toggleColumn = (key: keyof typeof visibleColumns) => {
314 setVisibleColumns((prev) => ({
315 ...prev,
316 [key]: !prev[key],
317 }));
318 };
319
320 // Format date
321 const formatDate = (dateString: string | null | undefined): string => {
322 if (!dateString) return "--";
323 try {
324 const date = new Date(dateString);
325 return date.toLocaleDateString("en-US", {
326 year: "numeric",
327 month: "short",
328 day: "numeric",
329 });
330 } catch {
331 return dateString;
332 }
333 };
334
335 // Get status badge
336 const getStatusBadge = (status: string) => {
337 const statusConfig: Record<
338 string,
339 { label: string; className: string; icon: React.ReactNode }
340 > = {
341 active: {
342 label: __("Active", "yatra"),
343 className:
344 "bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400",
345 icon: <CheckCircle className="w-3 h-3" />,
346 },
347 inactive: {
348 label: __("Inactive", "yatra"),
349 className:
350 "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-400",
351 icon: <XCircle className="w-3 h-3" />,
352 },
353 };
354
355 const config = statusConfig[status] || statusConfig.inactive;
356 return (
357 <Badge className={`${config.className} flex items-center gap-1`}>
358 {config.icon}
359 {config.label}
360 </Badge>
361 );
362 };
363
364 // Define table columns (matching Specific Dates structure)
365 const tableColumns = useMemo(() => {
366 const cols = [];
367
368 if (visibleColumns.name) {
369 cols.push({
370 key: "name",
371 label: __("Rule Name", "yatra"),
372 visible: visibleColumns.name,
373 render: (rule: RecurringRule) => (
374 <div className="flex flex-col">
375 <button
376 onClick={() => onEditRule(rule.id)}
377 className="text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 text-left hover:underline"
378 >
379 {rule.name || formatRulePattern(rule)}
380 </button>
381 <span className="text-xs text-gray-500 dark:text-gray-400">
382 {rule.rule_type === "weekly"
383 ? __("Weekly", "yatra")
384 : rule.rule_type === "monthly"
385 ? __("Monthly", "yatra")
386 : __("Interval", "yatra")}
387 </span>
388 </div>
389 ),
390 });
391 }
392
393 if (visibleColumns.pattern) {
394 cols.push({
395 key: "pattern",
396 label: __("Pattern", "yatra"),
397 visible: visibleColumns.pattern,
398 render: (rule: RecurringRule) => (
399 <div className="flex items-center gap-1 text-sm text-gray-700 dark:text-gray-300">
400 <RefreshCw className="w-3 h-3" />
401 <span>{formatRulePattern(rule)}</span>
402 </div>
403 ),
404 });
405 }
406
407 if (visibleColumns.start_date) {
408 cols.push({
409 key: "start_date",
410 label: __("Start Date", "yatra"),
411 visible: visibleColumns.start_date,
412 render: (rule: RecurringRule) => (
413 <div className="text-sm text-gray-900 dark:text-white">
414 {formatDate(rule.start_date)}
415 </div>
416 ),
417 });
418 }
419
420 if (visibleColumns.end_date) {
421 cols.push({
422 key: "end_date",
423 label: __("End Date", "yatra"),
424 visible: visibleColumns.end_date,
425 render: (rule: RecurringRule) => (
426 <div className="text-sm text-gray-500 dark:text-gray-400">
427 {rule.end_date ? formatDate(rule.end_date) : __("Ongoing", "yatra")}
428 </div>
429 ),
430 });
431 }
432
433 if (visibleColumns.capacity) {
434 cols.push({
435 key: "capacity",
436 label: __("Capacity", "yatra"),
437 visible: visibleColumns.capacity,
438 render: (rule: RecurringRule) => (
439 <div className="flex flex-col items-center">
440 <span className="text-sm font-medium text-gray-900 dark:text-white">
441 {rule.seats_total || 0}
442 </span>
443 <span className="text-xs text-gray-500 dark:text-gray-400">
444 {__("total", "yatra")}
445 </span>
446 </div>
447 ),
448 });
449 }
450
451 if (visibleColumns.generated) {
452 cols.push({
453 key: "generated",
454 label: __("Generated", "yatra"),
455 visible: visibleColumns.generated,
456 render: (rule: RecurringRule) => (
457 <div className="flex flex-col items-center">
458 <span className="text-sm font-semibold text-blue-600 dark:text-blue-400">
459 {rule.generated_count || 0}
460 </span>
461 <span className="text-xs text-gray-500 dark:text-gray-400">
462 {__("dates", "yatra")}
463 </span>
464 </div>
465 ),
466 });
467 }
468
469 if (visibleColumns.price) {
470 cols.push({
471 key: "price",
472 label: __("Price", "yatra"),
473 visible: visibleColumns.price,
474 render: (rule: RecurringRule) => (
475 <div className="text-sm font-semibold text-gray-900 dark:text-white">
476 {rule.sale_price
477 ? formatYatraMoney(Number(rule.sale_price) || 0, adminCurrency, {
478 zeroAsUnknown: false,
479 })
480 : rule.original_price
481 ? formatYatraMoney(
482 Number(rule.original_price) || 0,
483 adminCurrency,
484 { zeroAsUnknown: false },
485 )
486 : formatYatraMoney(0, adminCurrency, { zeroAsUnknown: false })}
487 </div>
488 ),
489 });
490 }
491
492 if (visibleColumns.status) {
493 cols.push({
494 key: "status",
495 label: __("Status", "yatra"),
496 visible: visibleColumns.status,
497 render: (rule: RecurringRule) => getStatusBadge(rule.status),
498 });
499 }
500
501 return cols;
502 // eslint-disable-next-line react-hooks/exhaustive-deps
503 }, [
504 visibleColumns,
505 formatRulePattern,
506 formatDate,
507 getStatusBadge,
508 adminCurrency,
509 ]);
510
511 // Status toggle mutation
512 const toggleStatusMutation = useMutation({
513 mutationFn: async ({
514 id,
515 status,
516 }: {
517 id: number;
518 status: "active" | "inactive";
519 }) => {
520 return await apiClient.put(`/recurring-availability/${id}`, { status });
521 },
522 onSuccess: () => {
523 // Single prefix invalidation refreshes both the rules list
524 // and the status badge counts (now keyed under the same prefix).
525 queryClient.invalidateQueries({ queryKey: ["recurring-availability"] });
526 showToast(__("Rule status updated successfully", "yatra"), "success");
527 },
528 onError: (error: any) => {
529 showToast(
530 error?.message || __("Failed to update rule status", "yatra"),
531 "error",
532 );
533 },
534 });
535
536 // Table actions (matching Specific Dates structure)
537 const tableActions = useMemo(
538 () => [
539 {
540 key: "edit",
541 label: __("Edit", "yatra"),
542 icon: <Edit className="w-4 h-4" />,
543 onClick: (rule: RecurringRule) => onEditRule(rule.id),
544 },
545 {
546 key: "set-inactive",
547 label: __("Set Inactive", "yatra"),
548 icon: <XCircle className="w-4 h-4" />,
549 onClick: (rule: RecurringRule) => {
550 toggleStatusMutation.mutate({ id: rule.id, status: "inactive" });
551 },
552 condition: (rule: RecurringRule) => rule.status === "active",
553 },
554 {
555 key: "set-active",
556 label: __("Set Active", "yatra"),
557 icon: <CheckCircle className="w-4 h-4" />,
558 onClick: (rule: RecurringRule) => {
559 toggleStatusMutation.mutate({ id: rule.id, status: "active" });
560 },
561 condition: (rule: RecurringRule) => rule.status === "inactive",
562 },
563 {
564 key: "duplicate",
565 label: __("Duplicate", "yatra"),
566 icon: <Copy className="w-4 h-4" />,
567 onClick: (rule: RecurringRule) =>
568 setDuplicateConfirm({ isOpen: true, rule }),
569 },
570 {
571 key: "delete",
572 label: __("Delete", "yatra"),
573 icon: <Trash2 className="w-4 h-4" />,
574 onClick: (rule: RecurringRule) =>
575 setDeleteConfirm({ isOpen: true, rule }),
576 variant: "destructive" as const,
577 },
578 ],
579 [toggleStatusMutation, onEditRule],
580 );
581
582 return (
583 <div className="space-y-6">
584 {/* Confirmation Dialogs */}
585 <ConfirmationDialog
586 isOpen={deleteConfirm.isOpen}
587 onClose={() => setDeleteConfirm({ isOpen: false, rule: null })}
588 onConfirm={() => {
589 if (deleteConfirm.rule) {
590 deleteMutation.mutate(deleteConfirm.rule.id);
591 }
592 }}
593 title={__("Delete Recurring Rule", "yatra")}
594 message={
595 deleteConfirm.rule
596 ? __(
597 'Are you sure you want to delete the rule "{name}"? This action cannot be undone.',
598 "yatra",
599 ).replace(
600 "{name}",
601 deleteConfirm.rule.name ||
602 formatRulePattern(deleteConfirm.rule),
603 )
604 : __(
605 "Are you sure you want to delete this rule? This action cannot be undone.",
606 "yatra",
607 )
608 }
609 confirmText={__("Delete", "yatra")}
610 cancelText={__("Cancel", "yatra")}
611 variant="danger"
612 isLoading={deleteMutation.isPending}
613 />
614
615 <ConfirmationDialog
616 isOpen={duplicateConfirm.isOpen}
617 onClose={() => setDuplicateConfirm({ isOpen: false, rule: null })}
618 onConfirm={() => {
619 if (duplicateConfirm.rule) {
620 duplicateMutation.mutate(duplicateConfirm.rule.id);
621 }
622 }}
623 title={__("Duplicate Recurring Rule", "yatra")}
624 message={__(
625 "This will create a copy of this rule. You can edit it after creation.",
626 "yatra",
627 )}
628 confirmText={__("Duplicate", "yatra")}
629 cancelText={__("Cancel", "yatra")}
630 isLoading={duplicateMutation.isPending}
631 />
632
633 {/* Header */}
634 <div className="flex items-center justify-between">
635 <div className="flex items-center gap-3">
636 <h3 className="text-lg font-medium text-gray-900 dark:text-white">
637 {__("Recurring Rules", "yatra")}
638 </h3>
639 <Badge
640 className={
641 isSingleDayTrip
642 ? "bg-purple-100 text-purple-700 dark:bg-purple-900/20 dark:text-purple-400"
643 : "bg-indigo-100 text-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-400"
644 }
645 >
646 {isSingleDayTrip
647 ? __("Single-Day Trip", "yatra")
648 : __("Multi-Day Trip", "yatra")}
649 </Badge>
650 <Badge
651 className={
652 isTravelerBased
653 ? "bg-green-100 text-green-700 dark:bg-green-900/20 dark:text-green-400"
654 : "bg-gray-100 text-gray-700 dark:bg-gray-900/20 dark:text-gray-400"
655 }
656 >
657 {isTravelerBased
658 ? __("Traveler-Based Pricing", "yatra")
659 : __("Regular Pricing", "yatra")}
660 </Badge>
661 </div>
662 <Button variant="outline" onClick={onAddRule}>
663 <Plus className="w-4 h-4 mr-2" />
664 {isSingleDayTrip
665 ? __("Add Time Slots Rule", "yatra")
666 : __("Add Recurring Rule", "yatra")}
667 </Button>
668 </div>
669
670 <div className="text-sm text-gray-500 dark:text-gray-400">
671 {isSingleDayTrip
672 ? __(
673 "Create recurring time slots for your single-day trip (supports multiple time slots per day)",
674 "yatra",
675 )
676 : __(
677 "Automatically generate availability dates based on patterns",
678 "yatra",
679 )}
680 </div>
681
682 {/* Filters - Matching Specific Dates */}
683 <Card>
684 <CardContent className="pt-6">
685 <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
686 <div>
687 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
688 {__("Search", "yatra")}
689 </label>
690 <div className="relative">
691 <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
692 <Input
693 type="text"
694 value={searchTerm}
695 onChange={(e) => setSearchTerm(e.target.value)}
696 placeholder={__("Search rules...", "yatra")}
697 className="pl-10"
698 />
699 </div>
700 </div>
701 <div>
702 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
703 {__("Status", "yatra")}
704 </label>
705 <Select
706 value={statusFilter}
707 onChange={(e) => setStatusFilter(e.target.value)}
708 >
709 <option value="all">{__("All Status", "yatra")}</option>
710 <option value="active">{__("Active", "yatra")}</option>
711 <option value="inactive">{__("Inactive", "yatra")}</option>
712 </Select>
713 </div>
714 <div className="flex items-end">
715 <Button
716 variant="outline"
717 onClick={() => {
718 setSearchTerm("");
719 setStatusFilter("all");
720 }}
721 className="w-full"
722 >
723 <X className="w-4 h-4 mr-2" />
724 {__("Clear Filters", "yatra")}
725 </Button>
726 </div>
727 </div>
728 </CardContent>
729 </Card>
730
731 {/* Bulk Action Toolbar - Matching Specific Dates */}
732 <BulkActionToolbar
733 selectedIds={selectedIds}
734 bulkAction={bulkAction}
735 setBulkAction={setBulkAction}
736 onApply={handleBulkApply}
737 onClearSelection={() => setSelectedIds([])}
738 statusFilter={statusFilter}
739 setStatusFilter={setStatusFilter}
740 statusOptions={[
741 { key: "all", label: __("All", "yatra"), count: statusCounts.all },
742 {
743 key: "active",
744 label: __("Active", "yatra"),
745 count: statusCounts.active,
746 },
747 {
748 key: "inactive",
749 label: __("Inactive", "yatra"),
750 count: statusCounts.inactive,
751 },
752 ]}
753 showColumnsDropdown={showColumnsDropdown}
754 setShowColumnsDropdown={setShowColumnsDropdown}
755 columnOptions={[
756 {
757 key: "name",
758 label: __("Rule Name", "yatra"),
759 visible: visibleColumns.name,
760 },
761 {
762 key: "pattern",
763 label: __("Pattern", "yatra"),
764 visible: visibleColumns.pattern,
765 },
766 {
767 key: "start_date",
768 label: __("Start Date", "yatra"),
769 visible: visibleColumns.start_date,
770 },
771 {
772 key: "end_date",
773 label: __("End Date", "yatra"),
774 visible: visibleColumns.end_date,
775 },
776 {
777 key: "capacity",
778 label: __("Capacity", "yatra"),
779 visible: visibleColumns.capacity,
780 },
781 {
782 key: "generated",
783 label: __("Generated", "yatra"),
784 visible: visibleColumns.generated,
785 },
786 {
787 key: "price",
788 label: __("Price", "yatra"),
789 visible: visibleColumns.price,
790 },
791 {
792 key: "status",
793 label: __("Status", "yatra"),
794 visible: visibleColumns.status,
795 },
796 ]}
797 onToggleColumn={(columnKey: string) =>
798 toggleColumn(columnKey as keyof typeof visibleColumns)
799 }
800 bulkMutationPending={bulkDeleteMutation.isPending}
801 totalItems={rules.length}
802 bulkActionOptions={[{ value: "delete", label: __("Delete", "yatra") }]}
803 />
804
805 {/* Recurring Rules Section - Matching Specific Dates */}
806 <Card>
807 <CardContent className="pt-6">
808 {/* Section Header */}
809 <div className="mb-6">
810 <h3 className="text-lg font-semibold text-gray-900 dark:text-white">
811 {__("Recurring Rules", "yatra")}
812 </h3>
813 <p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
814 {tripName && (
815 <>
816 {__("Managing availability for", "yatra")}{" "}
817 <strong>{tripName}</strong>
818 {rules.length > 0 && (
819 <span className="ml-2">
820 ({rules.length}{" "}
821 {rules.length === 1
822 ? __("rule", "yatra")
823 : __("rules", "yatra")}
824 )
825 </span>
826 )}
827 </>
828 )}
829 </p>
830 </div>
831
832 <SharedTable
833 data={rules}
834 columns={tableColumns}
835 actions={tableActions}
836 isLoading={isLoading}
837 isError={false}
838 selectedItemIds={selectedIds}
839 onSelectItem={(id, checked) => {
840 if (checked) {
841 setSelectedIds([...selectedIds, id]);
842 } else {
843 setSelectedIds(selectedIds.filter((sid) => sid !== id));
844 }
845 }}
846 onSelectAll={(checked) => {
847 if (checked) {
848 setSelectedIds(rules.map((r: RecurringRule) => r.id));
849 } else {
850 setSelectedIds([]);
851 }
852 }}
853 isAllSelected={
854 selectedIds.length === rules.length && rules.length > 0
855 }
856 getItemId={(rule) => rule.id}
857 emptyText={__("No recurring rules found", "yatra")}
858 emptyDescription={__(
859 "Create your first recurring rule to get started",
860 "yatra",
861 )}
862 onCreateClick={onAddRule}
863 skeletonRows={5}
864 capability="yatra_view_trips"
865 />
866 </CardContent>
867 </Card>
868
869 {/* Info Box */}
870 <Card className="bg-blue-50 dark:bg-blue-900/10 border-blue-200 dark:border-blue-800">
871 <CardContent className="py-4">
872 <div className="flex items-start gap-3">
873 <AlertCircle className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
874 <div>
875 <h5 className="font-medium text-blue-900 dark:text-blue-200 mb-1">
876 {__("How Recurring Rules Work", "yatra")}
877 </h5>
878 <ul className="text-sm text-blue-800 dark:text-blue-300 space-y-1">
879 <li>
880 •{" "}
881 {__(
882 "Dates are generated automatically based on your patterns",
883 "yatra",
884 )}
885 </li>
886 <li>
887 •{" "}
888 {__(
889 "Specific dates (added manually) take priority over generated dates",
890 "yatra",
891 )}
892 </li>
893 <li>
894 •{" "}
895 {__(
896 "Use excluded dates to skip holidays or special occasions",
897 "yatra",
898 )}
899 </li>
900 <li>
901 •{" "}
902 {__(
903 "Bookings for generated dates create specific availability entries",
904 "yatra",
905 )}
906 </li>
907 </ul>
908 </div>
909 </div>
910 </CardContent>
911 </Card>
912 </div>
913 );
914 };
915
916 export default RecurringRules;
917