Merge pull request 'waseem' (#30) from waseem into development

Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_frontend/pulls/30
This commit is contained in:
Syed Waseem khadri Rafai
2026-08-11 07:30:28 +00:00
12 changed files with 422 additions and 216 deletions
+4 -1
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 608 B

@@ -99,7 +99,7 @@ export function submitActionPayload(payload: ActionSubmissionPayload): Promise<a
// ─── Master Data Options Loader ──────────────────────────────────────────────
export function getMasterDataOptions(categoryCode: string): Promise<{ label: string; value: string; id?: string }[]> {
return ApiClient.get<any, any[]>(`/master-data/category-values/${categoryCode}`)
return ApiClient.get<any, any[]>(`/master-data/${categoryCode}`)
.then((res) => {
if (Array.isArray(res)) {
return res.map((item) => {
@@ -49,7 +49,6 @@ export default function ActionBuilderPage() {
const [masterLookupOptions, setMasterLookupOptions] = useState<{ label: string; value: string }[]>([
{ label: 'None (Manual / Free-text)', value: '' },
{ label: 'Currencies (CURRENCIES)', value: 'CURRENCIES' },
]);
// Loading & Notification States
@@ -162,7 +161,6 @@ export default function ActionBuilderPage() {
}));
setMasterLookupOptions([
{ label: 'None (Manual / Free-text)', value: '' },
{ label: 'Currencies (CURRENCIES)', value: 'CURRENCIES' },
...opts,
]);
}
@@ -21,11 +21,22 @@ function normalizeItems(items: any[]): MasterDataItem[] {
});
}
// ─── Categories List Endpoint ───────────────────────────────────────────────
// ─── Lookup Tables & Categories List Endpoint ─────────────────────────────────
export function getLookupTables(): Promise<MasterDataCategoryItem[]> {
return ApiClient.get<any, MasterDataCategoryItem[]>('/master-data/lookup-tables')
.then((res) => (Array.isArray(res) ? res : []))
.catch(() => []);
}
export function getMasterCategories(): Promise<MasterDataCategoryItem[]> {
return ApiClient.get<any, MasterDataCategoryItem[]>('/master-data/categories')
.then((res) => (Array.isArray(res) ? res : []))
return getLookupTables()
.then((tables) => {
if (Array.isArray(tables) && tables.length > 0) return tables;
return ApiClient.get<any, MasterDataCategoryItem[]>('/master-data/categories')
.then((res) => (Array.isArray(res) ? res : []))
.catch(() => []);
})
.catch(() => []);
}
@@ -33,13 +44,9 @@ export function getMasterCategories(): Promise<MasterDataCategoryItem[]> {
export function getCategoryValues(categoryCode: string): Promise<MasterDataItem[]> {
if (!categoryCode) return Promise.resolve([]);
return ApiClient.get<any, any[]>(`/master-data/category-values/${categoryCode}`)
return ApiClient.get<any, any[]>(`/master-data/${categoryCode}`)
.then((res) => normalizeItems(res))
.catch(() =>
ApiClient.get<any, any[]>(`/master-data/${categoryCode}`)
.then((res) => normalizeItems(res))
.catch(() => []),
);
.catch(() => []);
}
// ─── Generic Master Data Item CRUD ──────────────────────────────────────────
@@ -121,11 +121,11 @@ export const MasterItemTable: React.FC<MasterItemTableProps> = ({
</td>
<td className="py-3 px-4 font-bold text-[#0F172B]">
{item.value || item.label || item.name}
{item.label || item.value}
</td>
<td className="py-3 px-4 font-mono text-slate-500 text-[12px]">
{item.code || '—'}
{item.value || '—'}
</td>
<td className="py-3 px-4">
@@ -182,11 +182,10 @@ export const MasterItemTable: React.FC<MasterItemTableProps> = ({
<button
key={page}
onClick={() => onPageChange(page)}
className={`min-w-[32px] h-8 px-2 rounded-lg text-[13px] font-semibold transition-colors ${
currentPage === page
? 'bg-[#1E7D5C] text-white shadow-sm font-bold'
: 'bg-white text-slate-600 border border-gray-200 hover:bg-slate-50'
}`}
className={`min-w-[32px] h-8 px-2 rounded-lg text-[13px] font-semibold transition-colors ${currentPage === page
? 'bg-[#1E7D5C] text-white shadow-sm font-bold'
: 'bg-white text-slate-600 border border-gray-200 hover:bg-slate-50'
}`}
>
{page}
</button>
+40 -12
View File
@@ -45,13 +45,9 @@ function normalizeItems(items: any[]): any[] {
function fetchCategoryValues(categoryCodeOrId: string): Promise<any[]> {
if (!categoryCodeOrId) return Promise.resolve([]);
return ApiClient.get<any, any[]>(`/master-data/category-values/${categoryCodeOrId}`)
return ApiClient.get<any, any[]>(`/master-data/${categoryCodeOrId}`)
.then((res) => normalizeItems(res))
.catch(() =>
ApiClient.get<any, any[]>(`/master-data/${categoryCodeOrId}`)
.then((res) => normalizeItems(res))
.catch(() => []),
);
.catch(() => []);
}
function fetchMastersCategories(): Promise<any[]> {
@@ -187,17 +183,18 @@ export function getConditionOptions(categoryCodeOrId?: string): Promise<OptionIt
}
export function getOperatorOptions(): Promise<OptionItem[]> {
return fetchCategoryValues('operator')
.then((items) =>
(items || [])
return ApiClient.get<any, any[]>('/master-data/operators')
.then((res) => {
const items = Array.isArray(res) ? res : [];
return items
.filter((i) => i.isActive !== false)
.map((i) => ({
label: i.label || i.name || i.value || '',
label: i.name || i.symbol || i.label || i.code || '',
value: i.id || i.code || i.value || '',
id: i.id,
code: i.code,
})),
)
}));
})
.catch(() => []);
}
@@ -294,3 +291,34 @@ export function updatePolicyStatus(id: string, status: string): Promise<PolicyEn
export function deletePolicy(id: string): Promise<void> {
return ApiClient.delete<any, void>(`/policy-engine/${id}`);
}
// ─── 3-Level Metadata API Functions ──────────────────────────────────────────
export function getConditionGroupsForCategory(categoryCodeOrId: string): Promise<any[]> {
if (!categoryCodeOrId) return Promise.resolve([]);
return ApiClient.get<any, any[]>(`/master-data/rule-categories/${categoryCodeOrId}/condition-groups`)
.then((res) => (Array.isArray(res) ? res : []))
.catch(() => []);
}
export function getConditionFieldsForGroup(groupIdOrCode: string): Promise<any[]> {
if (!groupIdOrCode) return Promise.resolve([]);
return ApiClient.get<any, any[]>(`/master-data/condition-groups/${groupIdOrCode}/fields`)
.then((res) => (Array.isArray(res) ? res : []))
.catch(() => []);
}
export function getFieldLookupOptions(fieldIdOrCode: string): Promise<OptionItem[]> {
if (!fieldIdOrCode) return Promise.resolve([]);
return ApiClient.get<any, any[]>(`/master-data/condition-fields/${fieldIdOrCode}/lookup-values`)
.then((res) =>
(res || []).map((i) => ({
label: i.label || i.name || i.value || '',
value: i.id || i.code || i.value || '',
id: i.id,
code: i.code,
})),
)
.catch(() => []);
}
+28
View File
@@ -12,3 +12,31 @@ export interface PaginatedPolicyEngineResponse {
totalPages: number;
page: number;
}
export interface ConditionGroupItem {
id: string;
code: string;
name: string;
displayOrder?: number;
isActive?: boolean;
}
export interface ConditionFieldItem {
id: string;
groupId: string;
code: string;
name: string;
lookupTable?: string;
dataType: 'STRING' | 'NUMBER' | 'BOOLEAN' | 'ENUM' | 'DATE';
operatorType: 'COMPARISON' | 'TEXT' | 'SET' | 'BOOLEAN';
displayOrder?: number;
isActive?: boolean;
}
export interface LookupOptionItem {
id: string;
code: string;
label: string;
value: string;
displayOrder?: number;
}
@@ -30,6 +30,9 @@ import {
getPolicy,
createPolicy,
updatePolicy,
getConditionGroupsForCategory,
getConditionFieldsForGroup,
getFieldLookupOptions,
type OptionItem,
type ActionTypeField,
} from '../PolicyEngineApi';
@@ -75,7 +78,6 @@ type Rule = {
const LOGIC_OPTIONS = [
{ label: 'AND', value: 'AND' },
{ label: 'OR', value: 'OR' },
{ label: 'NOT', value: 'NOT' },
];
// Per-gate color scheme (exact Figma tokens): AND=blue, OR=yellow, NOT=red.
@@ -175,15 +177,76 @@ export default function AddPolicyEngine() {
getActionCategoryOptions().then(setActionCategoryOptions);
}, []);
const handleCategoryChange = (ruleId: string, category: string) => {
handleUpdateRule(ruleId, { category });
if (category && !categoryConditionMap[category]) {
getConditionOptions(category).then((opts) => {
const [conditionFieldsMetaMap, setConditionFieldsMetaMap] = useState<Record<string, any>>({});
const [conditionValueLookupMap, setConditionValueLookupMap] = useState<Record<string, OptionItem[]>>({});
const loadCategoryConditions = async (category: string) => {
if (!category) return [];
try {
const groups = await getConditionGroupsForCategory(category);
const allFields: OptionItem[] = [];
const metaMap: Record<string, any> = {};
for (const g of groups) {
const fields = await getConditionFieldsForGroup(g.id || g.code);
fields.forEach((f: any) => {
const val = f.id || f.code;
allFields.push({
label: `${g.name} ${f.name}`,
value: val,
id: f.id,
code: f.code,
});
metaMap[val] = f;
});
}
setCategoryConditionMap((prev) => ({
...prev,
[category]: allFields,
}));
setConditionFieldsMetaMap((prev) => ({
...prev,
...metaMap,
}));
return allFields;
} catch (err) {
console.error('Failed to load condition groups/fields for category', err);
try {
const fallbackOpts = await getConditionOptions(category);
setCategoryConditionMap((prev) => ({
...prev,
[category]: opts,
[category]: fallbackOpts,
}));
});
return fallbackOpts;
} catch {
return [];
}
}
};
const handleCategoryChange = async (ruleId: string, category: string) => {
handleUpdateRule(ruleId, { category });
if (category) {
await loadCategoryConditions(category);
}
};
const handleConditionFieldSelect = async (ruleId: string, conditionId: string, selectedFieldIdOrCode: string) => {
handleUpdateCondition(ruleId, conditionId, { condition: selectedFieldIdOrCode, value: '' });
if (selectedFieldIdOrCode) {
try {
const opts = await getFieldLookupOptions(selectedFieldIdOrCode);
if (opts && opts.length > 0) {
setConditionValueLookupMap((prev) => ({
...prev,
[selectedFieldIdOrCode]: opts,
}));
}
} catch (err) {
console.error('Failed to load lookup options for condition field', err);
}
}
};
@@ -324,22 +387,33 @@ export default function AddPolicyEngine() {
const cat = matchedRuleCat ? matchedRuleCat.value : rawCat;
if (cat) {
getConditionOptions(cat).then((opts) => {
setCategoryConditionMap((prev) => ({ ...prev, [cat]: opts }));
});
await loadCategoryConditions(cat);
}
const loadedConditions: Condition[] =
Array.isArray(r.conditions) && r.conditions.length > 0
? r.conditions.map((c: any) => ({
id: c.id || crypto.randomUUID(),
condition: c.fieldId || c.condition || '',
operator: c.operatorId || c.operator || '',
value: c.valueText || c.value || '',
logic: c.logicalOperator || c.logic || 'AND',
}))
id: c.id || crypto.randomUUID(),
condition: c.fieldId || c.condition || '',
operator: c.operatorId || c.operator || '',
value: c.valueText || c.value || '',
logic: c.logicalOperator || c.logic || 'AND',
}))
: [{ id: crypto.randomUUID(), condition: '', operator: '', value: '', logic: 'AND' }];
for (const c of loadedConditions) {
if (c.condition) {
try {
const opts = await getFieldLookupOptions(c.condition);
if (opts && opts.length > 0) {
setConditionValueLookupMap((prev) => ({ ...prev, [c.condition]: opts }));
}
} catch (err) {
console.error('Failed to load lookup options for condition', err);
}
}
}
const loadedActions: Action[] = [];
if (Array.isArray(r.actions) && r.actions.length > 0) {
for (const a of r.actions) {
@@ -454,9 +528,9 @@ export default function AddPolicyEngine() {
targetAudiences:
audienceType === 'Selected Cohorts'
? selectedCohorts.map((cohortId) => ({
targetType: 'COHORT',
targetId: String(cohortId),
}))
targetType: 'COHORT',
targetId: String(cohortId),
}))
: [],
rules: rules.map((r, rIdx) => ({
ruleCategoryId:
@@ -862,69 +936,99 @@ export default function AddPolicyEngine() {
<div className="mb-8">
<h4 className="text-[14px] font-bold text-[#0F172B] mb-4">Visual Condition Builder</h4>
<div className="flex flex-col gap-3">
{rule.conditions.map((condition, cIdx) => (
<div key={condition.id} className="flex items-end gap-3">
<div className="flex-1 flex flex-col gap-2">
<CustomDropdown
label='Condition'
options={categoryConditionMap[rule.category] || []}
value={condition.condition}
onChange={(val) => handleUpdateCondition(rule.id, condition.id, { condition: val })}
placeholder={rule.category ? "Select Condition..." : "Select Rule Category first"}
disabled={!rule.category}
/>
</div>
<div className="flex-1 flex flex-col gap-2">
<CustomDropdown
label='Operator'
options={operatorOptions}
value={condition.operator}
onChange={(val) => handleUpdateCondition(rule.id, condition.id, { operator: val })}
placeholder="Selected Option"
/>
</div>
<div className="flex-1 flex flex-col gap-2">
<CustomInput
label='Value'
value={condition.value}
onChange={(e) => handleUpdateCondition(rule.id, condition.id, { value: e.target.value })}
placeholder="Selected Option"
/>
</div>
{cIdx < rule.conditions.length - 1 ? (
<div className="w-[163px] flex flex-col gap-2">
<label className="text-[14px] font-medium leading-none text-[#001811]">Logic</label>
<LogicDropdown
value={condition.logic}
onChange={(val) => handleUpdateCondition(rule.id, condition.id, { logic: val })}
{rule.conditions.map((condition, cIdx) => {
const fieldMeta = conditionFieldsMetaMap[condition.condition];
const lookupOpts = conditionValueLookupMap[condition.condition] || [];
const isLookup = (fieldMeta && (fieldMeta.lookupTable || fieldMeta.dataType === 'ENUM')) || lookupOpts.length > 0;
const isBoolean = fieldMeta?.dataType === 'BOOLEAN';
const isNumber = fieldMeta?.dataType === 'NUMBER';
return (
<div key={condition.id} className="flex items-end gap-3">
<div className="flex-1 flex flex-col gap-2">
<CustomDropdown
label='Condition'
options={categoryConditionMap[rule.category] || []}
value={condition.condition}
onChange={(val) => handleConditionFieldSelect(rule.id, condition.id, val)}
placeholder={rule.category ? "Select Condition..." : "Select Rule Category first"}
disabled={!rule.category}
/>
</div>
) : (
<div className="w-[163px] flex items-center">
<CustomButton
variant="secondary"
className="!w-full !justify-center !bg-[#EAFAF5] hover:!bg-[#d9ece4] !px-4 !gap-2.5 !h-[49px] !rounded-[12px] whitespace-nowrap"
leftIcon={<PlusIcon size={16} color="#14704E" />}
onClick={() => handleAddCondition(rule.id)}
>
<span className="font-bold text-[14px] leading-none text-center bg-gradient-to-b from-[#1B9869] to-[#14704E] bg-clip-text text-transparent">
Add Condition
</span>
</CustomButton>
<div className="flex-1 flex flex-col gap-2">
<CustomDropdown
label='Operator'
options={operatorOptions}
value={condition.operator}
onChange={(val) => handleUpdateCondition(rule.id, condition.id, { operator: val })}
placeholder="Selected Option"
/>
</div>
)}
<div className="flex items-center h-11">
{rule.conditions.length > 1 && (
<button
onClick={() => handleDeleteCondition(rule.id, condition.id)}
className="w-11 h-11 flex items-center justify-center bg-red-50 text-[#D40000] rounded-[10px] hover:bg-red-100 transition-colors"
>
<TrashIcon size={16} strokeWidth={1.5} color="#D40000" />
</button>
<div className="flex-1 flex flex-col gap-2">
{isLookup ? (
<CustomDropdown
label='Value'
options={lookupOpts}
value={condition.value}
onChange={(val) => handleUpdateCondition(rule.id, condition.id, { value: val })}
placeholder="Select Value..."
/>
) : isBoolean ? (
<CustomDropdown
label='Value'
options={[
{ label: 'True', value: 'true' },
{ label: 'False', value: 'false' },
]}
value={condition.value}
onChange={(val) => handleUpdateCondition(rule.id, condition.id, { value: val })}
placeholder="Select..."
/>
) : (
<CustomInput
label='Value'
type={isNumber ? 'number' : 'text'}
value={condition.value}
onChange={(e) => handleUpdateCondition(rule.id, condition.id, { value: e.target.value })}
placeholder="Enter Value..."
/>
)}
</div>
{cIdx < rule.conditions.length - 1 ? (
<div className="w-[163px] flex flex-col gap-2">
<label className="text-[14px] font-medium leading-none text-[#001811]">Logic</label>
<LogicDropdown
value={condition.logic}
onChange={(val) => handleUpdateCondition(rule.id, condition.id, { logic: val })}
/>
</div>
) : (
<div className="w-[163px] flex items-center">
<CustomButton
variant="secondary"
className="!w-full !justify-center !bg-[#EAFAF5] hover:!bg-[#d9ece4] !px-4 !gap-2.5 !h-[49px] !rounded-[12px] whitespace-nowrap"
leftIcon={<PlusIcon size={16} color="#14704E" />}
onClick={() => handleAddCondition(rule.id)}
>
<span className="font-bold text-[14px] leading-none text-center bg-gradient-to-b from-[#1B9869] to-[#14704E] bg-clip-text text-transparent">
Add Condition
</span>
</CustomButton>
</div>
)}
<div className="flex items-center h-11">
{rule.conditions.length > 1 && (
<button
onClick={() => handleDeleteCondition(rule.id, condition.id)}
className="w-11 h-11 flex items-center justify-center bg-red-50 text-[#D40000] rounded-[10px] hover:bg-red-100 transition-colors"
>
<TrashIcon size={16} strokeWidth={1.5} color="#D40000" />
</button>
)}
</div>
</div>
</div>
))}
)
})}
</div>
</div>
@@ -61,6 +61,10 @@ export function updateRecoveryIncident(id: string, data: Partial<RecoveryInciden
return ApiClient.patch<any, RecoveryIncident>(`/recovery-incidents/${id}`, data);
}
export function updateIncidentStatus(id: string, status: string): Promise<RecoveryIncident> {
return ApiClient.patch<any, RecoveryIncident>(`/recovery-incidents/${id}/status`, { status });
}
export function deleteRecoveryIncident(id: string): Promise<void> {
return ApiClient.delete<any, void>(`/recovery-incidents/${id}`);
}
@@ -180,11 +180,10 @@ export default function RecoveryIncidentsList() {
const handleStatusChange = async (incident: RecoveryIncident, text: string) => {
try {
const { updateRecoveryIncident } = await import('../RecoveryIncidentsApi');
await updateRecoveryIncident(incident.id, {
status: text
});
const { updateIncidentStatus, getRecoveryMetrics } = await import('../RecoveryIncidentsApi');
await updateIncidentStatus(incident.id, text);
fetchIncidents();
getRecoveryMetrics().then((data) => setMetrics(data)).catch(() => {});
} catch (error) {
console.error("Failed to update status", error);
}
@@ -9,7 +9,7 @@ export default function RecoveryPlanTab() {
<CreditCardIcon size={20} weight="bold" className="text-[#143d30]" />
<h3 className="text-[13px] font-bold text-[#143d30] tracking-wider">FINANCIAL REFUND</h3>
</div>
<div className="grid grid-cols-3 gap-6">
<div>
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">REFUND AMOUNT</span>
@@ -32,7 +32,7 @@ export default function RecoveryPlanTab() {
<GiftIcon size={20} weight="bold" className="text-[#143d30]" />
<h3 className="text-[13px] font-bold text-[#143d30] tracking-wider">COMPENSATION & PERKS</h3>
</div>
<div className="grid grid-cols-3 gap-6">
<div>
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">CASH COMPENSATION</span>
@@ -55,7 +55,7 @@ export default function RecoveryPlanTab() {
<HandHeartIcon size={20} weight="bold" className="text-[#143d30]" />
<h3 className="text-[13px] font-bold text-[#143d30] tracking-wider">PASSENGER CARE</h3>
</div>
<div className="grid grid-cols-3 gap-6">
<div>
<span className="block text-[11px] font-bold text-gray-400 tracking-wider mb-1">MEAL VOUCHERS</span>
+136 -100
View File
@@ -1,37 +1,62 @@
import { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import {User, Checks, X, ClockCounterClockwiseIcon, ArrowLeftIcon, ArrowsClockwiseIcon, AirplaneTiltIcon } from '@phosphor-icons/react';
import { User, Checks, X, ClockCounterClockwiseIcon, ArrowLeftIcon, ArrowsClockwiseIcon, AirplaneTiltIcon } from '@phosphor-icons/react';
import { CustomButton, CustomTabs, CustomBackButton, CustomStatus } from '../../../components/custom';
import SummaryTab from './SummaryTab';
import CaseDetailsTab from './CaseDetailsTab';
import RecoveryPlanTab from './RecoveryPlanTab';
import AuditTrailTab from './AuditTrailTab';
import { SparkleIcon } from 'lucide-react';
import { getRecoveryIncident } from '../RecoveryIncidentsApi';
import { getRecoveryIncident, updateIncidentStatus } from '../RecoveryIncidentsApi';
import type { RecoveryIncident } from '../RecoveryIncidentsTypes';
function getStatusVariant(status?: string): "success" | "error" | "warning" | "info" | "neutral" {
if (!status) return "neutral";
const s = status.toLowerCase();
if (s.includes("appr") || s.includes("active") || s.includes("success")) return "success";
if (s.includes("reject") || s.includes("denied") || s.includes("error")) return "error";
if (s.includes("pend") || s.includes("review") || s.includes("warn")) return "warning";
if (s.includes("new") || s.includes("info")) return "info";
return "neutral";
}
export default function RecoveryIncidentTabs() {
const { id } = useParams();
const [activeTab, setActiveTab] = useState('Summary');
const [incident, setIncident] = useState<RecoveryIncident | null>(null);
const [loading, setLoading] = useState(true);
const [updating, setUpdating] = useState(false);
const fetchIncident = async () => {
if (!id) return;
setLoading(true);
try {
const data = await getRecoveryIncident(id);
setIncident(data);
} catch (err) {
console.error("Failed to load incident:", err);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (id) {
setLoading(true);
getRecoveryIncident(id)
.then(data => {
setIncident(data);
})
.catch(err => {
console.error("Failed to load incident:", err);
})
.finally(() => {
setLoading(false);
});
}
fetchIncident();
}, [id]);
const handleStatusChange = async (newStatus: string) => {
if (!id || updating) return;
setUpdating(true);
try {
const updated = await updateIncidentStatus(id, newStatus);
setIncident(updated);
} catch (err) {
console.error("Failed to update incident status:", err);
} finally {
setUpdating(false);
}
};
const tabItems = [
{
id: 'Summary',
@@ -59,6 +84,8 @@ export default function RecoveryIncidentTabs() {
return <div className="p-8 text-center text-gray-500">Loading incident...</div>;
}
const currentStatus = incident?.status || "Pending";
return (
<div className="w-full flex flex-col h-full relative">
{/* Header */}
@@ -67,8 +94,11 @@ export default function RecoveryIncidentTabs() {
<div className="flex items-center gap-3">
<CustomBackButton />
<h1 className="text-xl font-bold text-gray-900">{incident?.recoveryCode || id}</h1>
<CustomStatus status="UNDER REVIEW" variant="warning" className="uppercase !text-[11px] !px-2.5 !py-1 !tracking-wide" />
<CustomStatus status="HIGH PRIORITY" variant="error" className="uppercase !text-[11px] !px-2.5 !py-1 !bg-red-100 !text-red-800 !tracking-wide" />
<CustomStatus
status={currentStatus}
variant={getStatusVariant(currentStatus)}
className="uppercase !text-[11px] !px-2.5 !py-1 !tracking-wide"
/>
</div>
<div className="flex items-center gap-3 text-sm text-gray-500 ml-8">
<div className="flex items-center gap-1.5"><User size={16} /> {incident?.passengerName || 'Unknown'}</div>
@@ -87,14 +117,14 @@ export default function RecoveryIncidentTabs() {
{/* Tabs Row */}
<div className="flex items-center justify-between py-6">
<div className="flex-1">
<CustomTabs
tabs={tabItems}
value={activeTab}
onChange={setActiveTab}
contentClassName="!hidden"
/>
<CustomTabs
tabs={tabItems}
value={activeTab}
onChange={setActiveTab}
contentClassName="!hidden"
/>
</div>
<div className="flex items-center gap-3">
<CustomButton variant="outlined" className="!border-[#1B9869] !text-[#1B9869] hover:!bg-green-50 !font-semibold !rounded-lg">
Share with Finance
@@ -109,94 +139,100 @@ export default function RecoveryIncidentTabs() {
<div className="flex gap-6 items-start pb-24">
{/* Left Column - Tab Content */}
<div className="flex-1 bg-[#F8F9FA] rounded-[24px] p-3 border border-gray-100">
<CustomTabs
tabs={tabItems}
value={activeTab}
onChange={setActiveTab}
tabListClassName="!hidden"
contentClassName="!mt-0"
/>
<CustomTabs
tabs={tabItems}
value={activeTab}
onChange={setActiveTab}
tabListClassName="!hidden"
contentClassName="!mt-0"
/>
</div>
{/* Right Column - Sidebar */}
<div className="w-[360px] flex-shrink-0 bg-[#F8F9FA] rounded-[16px] border border-gray-100 relative">
{/* Blur Overlay */}
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white/20 backdrop-blur-[3px] rounded-[16px]">
<h4 className="text-[18px] font-bold text-[#143d30] italic">"Coming soon"</h4>
</div>
{/* Sidebar Content */}
<div className="p-6 select-none pointer-events-none">
<div className="flex items-center gap-2 mb-6">
<span className="text-[#1B9869]"><SparkleIcon size={20} height="fill" /></span>
<h3 className="text-[13px] font-bold text-gray-400 tracking-wider">AI RECOMMENDATION</h3>
</div>
<div className="mb-6">
<div className="flex justify-between items-end mb-2">
<span className="text-xs font-bold text-gray-400 tracking-wider">SATISFACTION PREDICT</span>
<span className="text-sm font-bold text-[#1B9869]">84%</span>
</div>
<div className="h-2 bg-white rounded-full overflow-hidden border border-gray-100">
<div className="h-full bg-[#1B9869] w-[84%] rounded-full opacity-60"></div>
</div>
</div>
{/* Blur Overlay */}
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white/20 backdrop-blur-[3px] rounded-[16px]">
<h4 className="text-[18px] font-bold text-[#143d30] italic">"Coming soon"</h4>
</div>
<div className="mb-8">
<div className="flex justify-between items-end mb-2">
<span className="text-xs font-bold text-gray-400 tracking-wider">ESCALATION RISK</span>
<span className="text-sm font-bold text-blue-500">12%</span>
</div>
<div className="h-2 bg-white rounded-full overflow-hidden border border-gray-100">
<div className="h-full bg-blue-500 w-[12%] rounded-full opacity-60"></div>
</div>
</div>
{/* Sidebar Content */}
<div className="p-6 select-none pointer-events-none">
<div className="flex items-center gap-2 mb-6">
<span className="text-[#1B9869]"><SparkleIcon size={20} height="fill" /></span>
<h3 className="text-[13px] font-bold text-gray-400 tracking-wider">AI RECOMMENDATION</h3>
</div>
<div className="pt-6 border-t border-gray-200">
<h3 className="text-[13px] font-bold text-gray-400 tracking-wider mb-4">NEXT RECOMMENDED ACTION</h3>
<div className="bg-white rounded-xl p-5 mb-4 border border-gray-100 shadow-sm">
<p className="text-sm text-gray-500 text-center">Approve the automated recovery payout of [250 EUR]. This will prevent a regulatory complaint and retain this high-value Platinum member.</p>
</div>
<CustomButton
disabled
rightIcon={<ArrowLeftIcon size={16} weight="bold" className="rotate-180" />}
className="w-full !py-3 !bg-[#1B9869]/50 !text-white !font-semibold !rounded-lg"
>
EXECUTE RECOMMENDATION
</CustomButton>
</div>
</div>
<div className="mb-6">
<div className="flex justify-between items-end mb-2">
<span className="text-xs font-bold text-gray-400 tracking-wider">SATISFACTION PREDICT</span>
<span className="text-sm font-bold text-[#1B9869]">84%</span>
</div>
<div className="h-2 bg-white rounded-full overflow-hidden border border-gray-100">
<div className="h-full bg-[#1B9869] w-[84%] rounded-full opacity-60"></div>
</div>
</div>
<div className="mb-8">
<div className="flex justify-between items-end mb-2">
<span className="text-xs font-bold text-gray-400 tracking-wider">ESCALATION RISK</span>
<span className="text-sm font-bold text-blue-500">12%</span>
</div>
<div className="h-2 bg-white rounded-full overflow-hidden border border-gray-100">
<div className="h-full bg-blue-500 w-[12%] rounded-full opacity-60"></div>
</div>
</div>
<div className="pt-6 border-t border-gray-200">
<h3 className="text-[13px] font-bold text-gray-400 tracking-wider mb-4">NEXT RECOMMENDED ACTION</h3>
<div className="bg-white rounded-xl p-5 mb-4 border border-gray-100 shadow-sm">
<p className="text-sm text-gray-500 text-center">Approve the automated recovery payout of [250 EUR]. This will prevent a regulatory complaint and retain this high-value Platinum member.</p>
</div>
<CustomButton
disabled
rightIcon={<ArrowLeftIcon size={16} weight="bold" className="rotate-180" />}
className="w-full !py-3 !bg-[#1B9869]/50 !text-white !font-semibold !rounded-lg"
>
EXECUTE RECOMMENDATION
</CustomButton>
</div>
</div>
</div>
</div>
{/* Bottom Sticky Action Bar */}
<div className="sticky bottom-[-24px] -mx-8 px-8 py-4 bg-white/90 backdrop-blur-md border-t border-gray-100 flex justify-between items-center z-10 mt-auto shadow-[0_-10px_20px_-10px_rgba(0,0,0,0.05)]">
<div></div> {/* Spacer */}
<div className="flex gap-4">
<CustomButton
variant="text"
leftIcon={<X size={18} weight="bold" />}
className="!bg-[#FDE8E8] !text-[#E02424] !font-bold !rounded-lg !border !border-[#E02424] hover:!bg-red-100"
>
REJECT RECOVERY
</CustomButton>
<CustomButton
variant="text"
leftIcon={<ClockCounterClockwiseIcon size={18} weight="bold" />}
className="!bg-[#FEF3C7] !text-[#B45309] !font-bold !rounded-lg !border !border-[#B45309] hover:!bg-[#FDE68A]"
>
MARK FOR REVIEW
</CustomButton>
<CustomButton
variant="primary"
leftIcon={<Checks size={18} weight="bold" />}
className="!bg-[#1B9869] !bg-none !text-white !font-bold !rounded-lg !border !border-[#1B9869] hover:!bg-[#14704E]"
>
APPROVE RECOVERY
</CustomButton>
</div>
<div></div> {/* Spacer */}
<div className="flex gap-4">
<CustomButton
variant="text"
disabled={updating}
onClick={() => handleStatusChange("Rejected")}
leftIcon={<X size={18} weight="bold" />}
className="!bg-[#FDE8E8] !text-[#E02424] !font-bold !rounded-lg !border !border-[#E02424] hover:!bg-red-100 disabled:opacity-50"
>
REJECT RECOVERY
</CustomButton>
<CustomButton
variant="text"
disabled={updating}
onClick={() => handleStatusChange("Under Review")}
leftIcon={<ClockCounterClockwiseIcon size={18} weight="bold" />}
className="!bg-[#FEF3C7] !text-[#B45309] !font-bold !rounded-lg !border !border-[#B45309] hover:!bg-[#FDE68A] disabled:opacity-50"
>
MARK FOR REVIEW
</CustomButton>
<CustomButton
variant="primary"
disabled={updating}
onClick={() => handleStatusChange("Approved")}
leftIcon={<Checks size={18} weight="bold" />}
className="!bg-[#1B9869] !bg-none !text-white !font-bold !rounded-lg !border !border-[#1B9869] hover:!bg-[#14704E] disabled:opacity-50"
>
APPROVE RECOVERY
</CustomButton>
</div>
</div>
</div>
);