Merge pull request 'azeem' (#49) from azeem into development

Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_frontend/pulls/49
This commit is contained in:
Syed Waseem khadri Rafai
2026-08-21 09:55:23 +00:00
3 changed files with 115 additions and 51 deletions
@@ -17,7 +17,6 @@ import {
CustomLoader,
Skeleton,
} from '../../../components/custom';
import CustomSuccessModal from '../../../components/custom/CustomSuccessModal';
import {
getJurisdictionOptions,
getCohortOptions,
@@ -153,7 +152,6 @@ export default function AddPolicyEngine() {
const [loadingPolicy, setLoadingPolicy] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [showSuccessModal, setShowSuccessModal] = useState(false);
// Dynamic Options State (loaded directly from PolicyEngineApi)
const [jurisdictionOptions, setJurisdictionOptions] = useState<OptionItem[]>([]);
@@ -338,7 +336,7 @@ export default function AddPolicyEngine() {
// Policy Info State
const [policyName, setPolicyName] = useState('');
const [jurisdiction, setJurisdiction] = useState<(string | number)[]>([]);
const [status, setStatus] = useState<'Active' | 'Inactive'>('Active');
const [status, setStatus] = useState<'Active' | 'Inactive' | 'Draft'>('Active');
const [description, setDescription] = useState('');
// Target Audience State
@@ -381,7 +379,8 @@ export default function AddPolicyEngine() {
setJurisdiction(singleJur ? [singleJur] : []);
}
setStatus(
data.status?.toLowerCase() === 'active' ? 'Active' : 'Inactive'
data.status?.toLowerCase() === 'active' ? 'Active' :
data.status?.toLowerCase() === 'draft' ? 'Draft' : 'Inactive'
);
setDescription(data.description || '');
@@ -532,9 +531,49 @@ export default function AddPolicyEngine() {
.finally(() => setLoadingPolicy(false));
}, [policyId]);
const isFormValid = (() => {
if (!policyName.trim()) return false;
if (audienceType === 'Selected Cohorts' && (!selectedCohorts || selectedCohorts.length === 0)) return false;
if (!rules || rules.length === 0) return false;
for (const rule of rules) {
if (!rule.category) return false;
if (!rule.conditions || rule.conditions.length === 0) return false;
for (const cond of rule.conditions) {
if (!cond.condition || !cond.operator || cond.value === '' || cond.value === undefined || cond.value === null) return false;
}
if (!rule.actions || rule.actions.length === 0) return false;
for (const act of rule.actions) {
if (!act.actionCategoryId || !act.actionTypeId) return false;
const fields = actionTypeFieldsMap[act.actionTypeId] || [];
for (const field of fields) {
if (field.isRequired) {
const val = act.fieldValues?.[field.fieldCode];
if (val === undefined || val === null || val === '') return false;
if (Array.isArray(val) && val.length === 0) return false;
if (typeof val === 'object' && !Array.isArray(val)) {
if (val.amount === undefined || val.amount === null || val.amount === '') return false;
}
}
}
}
}
return true;
})();
const isDraftValid = policyName.trim().length > 0;
const handleSavePolicy = async (isDeploy: boolean) => {
if (!policyName.trim()) {
alert('Please enter a Policy Name.');
if (isDeploy && !isFormValid) {
return;
}
if (!isDeploy && !isDraftValid) {
return;
}
@@ -545,7 +584,7 @@ export default function AddPolicyEngine() {
jurisdictionId: Array.isArray(jurisdiction) && jurisdiction.length > 0 ? String(jurisdiction[0]) : undefined,
jurisdictionIds: Array.isArray(jurisdiction) ? jurisdiction.map(String) : [],
description: description || undefined,
status: isDeploy ? status.toLowerCase() : 'draft',
status: isDeploy ? (status === 'Draft' ? 'active' : status.toLowerCase()) : 'draft',
audienceType: audienceType === 'Selected Cohorts' ? 'COHORT' : 'ALL',
targetAudiences:
audienceType === 'Selected Cohorts'
@@ -621,7 +660,14 @@ export default function AddPolicyEngine() {
await createPolicy(payload);
}
setShowSuccessModal(true);
let actionWord = isDeploy ? 'created' : 'saved as draft';
if (isEditMode) {
actionWord = isDeploy ? 'updated' : 'saved as draft';
}
const successMsg = `Policy "${payload.policyName}" ${actionWord} successfully!`;
navigate('/policy-engine', { state: { successMsg } });
} catch (err) {
console.error('Failed to save policy', err);
alert('Failed to save policy. Please check input values and try again.');
@@ -883,6 +929,7 @@ export default function AddPolicyEngine() {
<div className="flex flex-col gap-2">
<CustomMultiSelect
label='Jurisdiction'
required
options={jurisdictionOptions}
value={jurisdiction}
onChange={setJurisdiction}
@@ -890,7 +937,7 @@ export default function AddPolicyEngine() {
/>
</div>
<div className="flex flex-col gap-2">
<label className="text-[13px] font-semibold text-gray-700">Status</label>
<label className="text-[13px] font-semibold text-gray-700">Status<span className="text-red-500 ml-1">*</span></label>
<div className="flex items-center gap-6 h-11">
<CustomRadio
name="status"
@@ -911,6 +958,7 @@ export default function AddPolicyEngine() {
<div className="flex flex-col gap-2">
<CustomTextArea
label='Description'
required
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Enter description..."
@@ -943,6 +991,7 @@ export default function AddPolicyEngine() {
<div className="w-1/3 flex flex-col gap-2">
<CustomMultiSelect
label='Cohort'
required
options={cohortOptions}
value={selectedCohorts}
onChange={setSelectedCohorts}
@@ -989,6 +1038,7 @@ export default function AddPolicyEngine() {
<div className="w-1/3 flex flex-col gap-2">
<CustomDropdown
label='Rule Category'
required
options={ruleCategoryOptions}
value={rule.category}
onChange={(val) => handleCategoryChange(rule.id, val)}
@@ -996,7 +1046,7 @@ export default function AddPolicyEngine() {
/>
</div>
<div className="flex flex-col gap-2">
<label className="text-[14px] font-medium leading-none text-[#001811]">Priority</label>
<label className="text-[14px] font-medium leading-none text-[#001811]">Priority<span className="text-red-500 ml-1">*</span></label>
{(() => {
const prevPriority = getPrevPriority(rule);
const nextPriority = getNextPriority(rule);
@@ -1044,6 +1094,7 @@ export default function AddPolicyEngine() {
<div className="flex-1 flex flex-col gap-2">
<CustomDropdown
label='Condition'
required
options={categoryConditionMap[rule.category] || []}
value={condition.condition}
onChange={(val) => handleConditionFieldSelect(rule.id, condition.id, val)}
@@ -1054,6 +1105,7 @@ export default function AddPolicyEngine() {
<div className="flex-1 flex flex-col gap-2">
<CustomDropdown
label='Operator'
required
options={operatorOptions}
value={condition.operator}
onChange={(val) => handleUpdateCondition(rule.id, condition.id, { operator: val })}
@@ -1064,6 +1116,7 @@ export default function AddPolicyEngine() {
{isLookup ? (
<CustomDropdown
label='Value'
required
options={lookupOpts}
value={condition.value}
onChange={(val) => handleUpdateCondition(rule.id, condition.id, { value: val })}
@@ -1072,6 +1125,7 @@ export default function AddPolicyEngine() {
) : isBoolean ? (
<CustomDropdown
label='Value'
required
options={[
{ label: 'True', value: 'true' },
{ label: 'False', value: 'false' },
@@ -1083,6 +1137,7 @@ export default function AddPolicyEngine() {
) : (
<CustomInput
label='Value'
required
type={isNumber ? 'number' : 'text'}
value={condition.value}
onChange={(e) => handleUpdateCondition(rule.id, condition.id, { value: e.target.value })}
@@ -1092,7 +1147,7 @@ export default function AddPolicyEngine() {
</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>
<label className="text-[14px] font-medium leading-none text-[#001811]">Logic<span className="text-red-500 ml-1">*</span></label>
<LogicDropdown
value={condition.logic}
onChange={(val) => handleUpdateCondition(rule.id, condition.id, { logic: val })}
@@ -1140,6 +1195,7 @@ export default function AddPolicyEngine() {
<div className="flex-1 flex flex-col gap-2">
<CustomDropdown
label="Action Category"
required
options={actionCategoryOptions}
value={action.actionCategoryId}
onChange={(val) => handleActionCategoryChange(rule.id, action.id, val)}
@@ -1150,6 +1206,7 @@ export default function AddPolicyEngine() {
<div className="flex-1 flex flex-col gap-2">
<CustomDropdown
label="Action Type"
required
options={actionTypesByCategoryMap[action.actionCategoryId] || []}
value={action.actionTypeId}
onChange={(val) => handleActionTypeChange(rule.id, action.id, val)}
@@ -1160,7 +1217,7 @@ export default function AddPolicyEngine() {
{aIdx < rule.actions.length - 1 ? (
<div className="w-[163px] flex flex-col gap-2">
<label className="text-[14px] font-medium leading-none text-[#001811]">Logic</label>
<label className="text-[14px] font-medium leading-none text-[#001811]">Logic<span className="text-red-500 ml-1">*</span></label>
<LogicDropdown value={action.logic} readOnly />
</div>
) : (
@@ -1329,16 +1386,14 @@ export default function AddPolicyEngine() {
<CustomStatus status={status} />
</div>
<div className="flex items-center gap-3">
{!isEditMode && (
<CustomButton
variant="secondary"
className="!text-[#1E7D5C] !bg-[#E8F3EF] hover:!bg-[#d9ece4] !border-none font-semibold px-6"
onClick={() => handleSavePolicy(false)}
disabled={isSaving}
>
{isSaving ? 'Saving...' : 'Save Draft'}
</CustomButton>
)}
<CustomButton
variant="secondary"
className="!text-[#1E7D5C] !bg-[#E8F3EF] hover:!bg-[#d9ece4] !border-none font-semibold px-6 disabled:opacity-50"
onClick={() => handleSavePolicy(false)}
disabled={isSaving || !isDraftValid}
>
{isSaving ? 'Saving...' : 'Save Draft'}
</CustomButton>
<CustomButton
variant="outlined"
className="!border-[#1E7D5C] !text-[#1E7D5C] hover:!bg-gray-50 font-semibold px-6"
@@ -1349,29 +1404,15 @@ export default function AddPolicyEngine() {
</CustomButton>
<CustomButton
variant="primary"
className="!bg-[#1E7D5C] hover:!bg-[#17664B] font-semibold px-6 shadow-sm"
className="!bg-[#1E7D5C] hover:!bg-[#17664B] font-semibold px-6 shadow-sm disabled:opacity-50"
onClick={() => handleSavePolicy(true)}
disabled={isSaving}
disabled={isSaving || !isFormValid}
>
{isSaving ? 'Deploying...' : isEditMode ? 'Update & Deploy Policy' : 'Deploy Policy'}
</CustomButton>
</div>
</div>
{/* ─── Success Modal ──────────────────────────────────────────────── */}
<CustomSuccessModal
isOpen={showSuccessModal}
onClose={() => {
setShowSuccessModal(false);
navigate('/policy-engine');
}}
title={isEditMode ? 'Policy Updated Successfully.' : 'Policy Created Successfully.'}
label="POLICY NAME"
cohortName={policyName}
cohortStatus={status}
cohortDescription={description}
/>
</div>
);
}
@@ -7,7 +7,7 @@ import {
PencilSimpleIcon,
ChecksIcon,
} from "@phosphor-icons/react";
import { useNavigate } from "react-router-dom";
import { useNavigate, useLocation } from "react-router-dom";
import {
CustomTable,
CustomInput,
@@ -65,6 +65,14 @@ export default function PolicyEngineList() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [successMsg, setSuccessMsg] = useState<string | null>(null);
const location = useLocation();
useEffect(() => {
if (location.state?.successMsg) {
setSuccessMsg(location.state.successMsg);
window.history.replaceState({}, document.title);
}
}, [location]);
// Pagination
const [currentPage, setCurrentPage] = useState(1);
@@ -95,19 +95,27 @@ export default function RecoveryIncidentsList() {
const [error, setError] = useState<string | null>(null);
const [successMsg, setSuccessMsg] = useState<string | null>(null);
useEffect(() => {
getRecoveryMetrics()
.then((data) => setMetrics(data))
.catch((err) => console.error("Failed to fetch recovery metrics:", err));
}, []);
// Pagination
const [currentPage, setCurrentPage] = useState(1);
// Filters
const [search, setSearch] = useState("");
const [isGrouped, setIsGrouped] = useState(false);
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(new Set());
const [isGrouped, setIsGrouped] = useState(() => {
return sessionStorage.getItem("recovery_isGrouped") === "true";
});
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(() => {
const saved = sessionStorage.getItem("recovery_collapsedGroups");
return saved ? new Set(JSON.parse(saved)) : new Set();
});
useEffect(() => {
sessionStorage.setItem("recovery_isGrouped", String(isGrouped));
}, [isGrouped]);
useEffect(() => {
sessionStorage.setItem("recovery_collapsedGroups", JSON.stringify(Array.from(collapsedGroups)));
}, [collapsedGroups]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingIncident, setEditingIncident] = useState<RecoveryIncident | null>(null);
@@ -129,6 +137,9 @@ export default function RecoveryIncidentsList() {
try {
const data = await getRecoveryIncidents();
setIncidents(data);
getRecoveryMetrics()
.then((m) => setMetrics(m))
.catch((err) => console.error("Failed to fetch recovery metrics:", err));
} catch (error) {
console.error("Failed to fetch recovery incidents", error);
setIncidents([]);
@@ -143,8 +154,13 @@ export default function RecoveryIncidentsList() {
useEffect(() => {
if (incidents.length > 0) {
const keys = new Set(incidents.map((i) => i.flightNumber || "Other"));
setCollapsedGroups(keys);
setCollapsedGroups((prev) => {
const hasSaved = sessionStorage.getItem("recovery_hasSavedGroups");
if (hasSaved) return prev;
sessionStorage.setItem("recovery_hasSavedGroups", "true");
return new Set(incidents.map((i) => i.flightNumber || "Other"));
});
}
}, [incidents]);
@@ -194,10 +210,9 @@ export default function RecoveryIncidentsList() {
try {
setError(null);
setSuccessMsg(null);
const { updateIncidentStatus, getRecoveryMetrics } = await import('../RecoveryIncidentsApi');
const { updateIncidentStatus } = await import('../RecoveryIncidentsApi');
await updateIncidentStatus(incident.id, text);
fetchIncidents();
getRecoveryMetrics().then((data) => setMetrics(data)).catch(() => { });
setSuccessMsg(`Incident status updated to ${text} successfully.`);
} catch (error) {
console.error("Failed to update status", error);