PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.8
Yatra – Travel Booking & Tour Operator Software v3.0.2.8
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 2.0.10 All 83 releases
yatra / resources / js / components / availability / RecurringRules.tsx

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

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