Enforced approval rights
This commit is contained in:
@@ -25,6 +25,7 @@ import {
|
||||
getAwards,
|
||||
getAward,
|
||||
createAward,
|
||||
updateAward,
|
||||
getBudgets,
|
||||
getBudget,
|
||||
createBudget,
|
||||
@@ -52,6 +53,7 @@ import {
|
||||
Disbursement,
|
||||
ApprovalStatus,
|
||||
CreateAwardRequest,
|
||||
UpdateAwardRequest,
|
||||
CreateBudgetRequest,
|
||||
UpdateBudgetRequest,
|
||||
CreateDisbursementRequest,
|
||||
@@ -77,6 +79,15 @@ const awardSchema = z.object({
|
||||
programmeId: z.coerce.number().min(1, 'Select funding programme'),
|
||||
});
|
||||
|
||||
const editAwardSchema = z.object({
|
||||
organisationId: z.coerce.number().min(1, 'Recipient organisation is required'),
|
||||
programmeId: z.coerce.number().min(1, 'Funding programme is required'),
|
||||
amount: z
|
||||
.coerce
|
||||
.number()
|
||||
.positive('Approved funding amount must be strictly greater than zero (no negative or zero values)'),
|
||||
});
|
||||
|
||||
const budgetSchema = z.object({
|
||||
projectId: z.coerce.number().min(1, 'Select project'),
|
||||
name: z.string().min(3, 'Budget category name required'),
|
||||
@@ -93,6 +104,7 @@ const disbursementSchema = z.object({
|
||||
});
|
||||
|
||||
type AwardFormValues = z.infer<typeof awardSchema>;
|
||||
type EditAwardFormValues = z.infer<typeof editAwardSchema>;
|
||||
type BudgetFormValues = z.infer<typeof budgetSchema>;
|
||||
type DisbursementFormValues = z.infer<typeof disbursementSchema>;
|
||||
|
||||
@@ -105,6 +117,9 @@ export const GrantsPage: React.FC = () => {
|
||||
|
||||
// Modals
|
||||
const [isAwardModalOpen, setIsAwardModalOpen] = useState(false);
|
||||
const [editingAward, setEditingAward] = useState<Award | null>(null);
|
||||
const [isEditAwardModalOpen, setIsEditAwardModalOpen] = useState(false);
|
||||
const [selectedAwardStatus, setSelectedAwardStatus] = useState<ApprovalStatus>(ApprovalStatus.Pending);
|
||||
const [isBudgetModalOpen, setIsBudgetModalOpen] = useState(false);
|
||||
const [editingBudget, setEditingBudget] = useState<Budget | null>(null);
|
||||
const [isEditBudgetModalOpen, setIsEditBudgetModalOpen] = useState(false);
|
||||
@@ -165,7 +180,7 @@ export const GrantsPage: React.FC = () => {
|
||||
|
||||
// Forms
|
||||
const awardForm = useForm<AwardFormValues>({
|
||||
resolver: zodResolver(awardSchema) as any,
|
||||
resolver: zodResolver(awardSchema),
|
||||
defaultValues: {
|
||||
referenceNo: '',
|
||||
projectId: projects[0]?.id ? Number(projects[0].id) : 401,
|
||||
@@ -176,8 +191,12 @@ export const GrantsPage: React.FC = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const editAwardForm = useForm<EditAwardFormValues>({
|
||||
resolver: zodResolver(editAwardSchema),
|
||||
});
|
||||
|
||||
const budgetForm = useForm<BudgetFormValues>({
|
||||
resolver: zodResolver(budgetSchema) as any,
|
||||
resolver: zodResolver(budgetSchema),
|
||||
defaultValues: {
|
||||
projectId: projects[0]?.id ? Number(projects[0].id) : 401,
|
||||
name: '',
|
||||
@@ -186,7 +205,7 @@ export const GrantsPage: React.FC = () => {
|
||||
});
|
||||
|
||||
const disbursementForm = useForm<DisbursementFormValues>({
|
||||
resolver: zodResolver(disbursementSchema) as any,
|
||||
resolver: zodResolver(disbursementSchema),
|
||||
defaultValues: {
|
||||
projectId: projects[0]?.id ? Number(projects[0].id) : 401,
|
||||
amount: 2500000,
|
||||
@@ -211,6 +230,21 @@ export const GrantsPage: React.FC = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const updateAwardMutation = useMutation({
|
||||
mutationFn: (data: UpdateAwardRequest) => updateAward(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['awards'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['dashboard'] });
|
||||
success('Award Updated', 'Award details saved successfully.');
|
||||
setIsEditAwardModalOpen(false);
|
||||
setEditingAward(null);
|
||||
editAwardForm.reset();
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
toastError('Award update failed', err);
|
||||
},
|
||||
});
|
||||
|
||||
const budgetMutation = useMutation({
|
||||
mutationFn: (data: CreateBudgetRequest) => createBudget(data),
|
||||
onSuccess: () => {
|
||||
@@ -837,18 +871,21 @@ export const GrantsPage: React.FC = () => {
|
||||
<SortableTh column="createdAt" currentColumn={awardSortCol} direction={awardSortDir} onSort={handleAwardSort} align="right" className="py-3.5 px-5 text-right">
|
||||
Date Registered
|
||||
</SortableTh>
|
||||
<th className="py-3.5 px-5 text-right font-semibold text-xs text-slate-400 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800/50">
|
||||
{loadingAwards ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="py-8 text-center text-slate-500">
|
||||
<td colSpan={7} className="py-8 text-center text-slate-500">
|
||||
Loading registered awards...
|
||||
</td>
|
||||
</tr>
|
||||
) : sortedAwards.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="py-8 text-center text-slate-500">
|
||||
<td colSpan={7} className="py-8 text-center text-slate-500">
|
||||
{searchTerm ? 'No awards match your search query.' : 'No registered awards found.'}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -878,6 +915,36 @@ export const GrantsPage: React.FC = () => {
|
||||
<td className="py-3.5 px-5 text-right font-mono text-slate-400">
|
||||
{formatDate(awd.createdAt)}
|
||||
</td>
|
||||
<td className="py-3.5 px-5 text-right">
|
||||
{Number(awd.status) === ApprovalStatus.Approved ? (
|
||||
<span
|
||||
className="inline-flex items-center gap-1 text-xs font-semibold text-emerald-400 bg-emerald-500/10 px-2.5 py-1.5 rounded-lg border border-emerald-500/20 cursor-default"
|
||||
title="Approved award is immutable. Details cannot be modified."
|
||||
>
|
||||
<Lock className="w-3 h-3 text-emerald-400" />
|
||||
<span>Approved (Immutable)</span>
|
||||
</span>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setEditingAward(awd);
|
||||
setSelectedAwardStatus(Number(awd.status) as ApprovalStatus);
|
||||
editAwardForm.reset({
|
||||
organisationId: Number(awd.organisationId),
|
||||
programmeId: Number(awd.programmeId),
|
||||
amount: Number(awd.amount),
|
||||
});
|
||||
setIsEditAwardModalOpen(true);
|
||||
}}
|
||||
className="inline-flex items-center gap-1 text-xs font-semibold text-slate-200 hover:text-white bg-slate-900/80 hover:bg-slate-800 border border-slate-700/80 px-2.5 py-1.5 rounded-lg transition shadow-xs cursor-pointer"
|
||||
title="Edit Award"
|
||||
>
|
||||
<Edit2 className="w-3 h-3" />
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
@@ -989,40 +1056,52 @@ export const GrantsPage: React.FC = () => {
|
||||
<td className="py-3.5 px-4 font-mono text-slate-400">{getUserEmail(b.createdBy)}</td>
|
||||
<td className="py-3.5 px-5 text-right">
|
||||
<div className="flex items-center justify-end gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setEditingBudget(b);
|
||||
setSelectedBudgetStatus(b.status ?? ApprovalStatus.Approved);
|
||||
setIsEditBudgetModalOpen(true);
|
||||
}}
|
||||
className="inline-flex items-center gap-1 text-xs font-semibold text-slate-200 hover:text-white bg-slate-900/80 hover:bg-slate-800 border border-slate-700/80 px-2.5 py-1.5 rounded-lg transition shadow-xs cursor-pointer"
|
||||
title="Update Budget Status"
|
||||
>
|
||||
<Edit2 className="w-3 h-3" />
|
||||
<span>Update</span>
|
||||
</button>
|
||||
{b.status !== ApprovalStatus.Approved && canApproveRecord('grant_financials', b.createdBy) && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={updateBudgetMutation.isPending}
|
||||
onClick={() => {
|
||||
updateBudgetMutation.mutate({
|
||||
budgetId: b.id,
|
||||
status: ApprovalStatus.Approved,
|
||||
});
|
||||
}}
|
||||
className="inline-flex items-center gap-1 text-xs font-semibold text-emerald-300 hover:text-white bg-emerald-500/10 hover:bg-emerald-500/20 border border-emerald-500/30 px-2.5 py-1.5 rounded-lg transition shadow-xs cursor-pointer"
|
||||
title="Approve Budget Line"
|
||||
{Number(b.status) === ApprovalStatus.Approved ? (
|
||||
<span
|
||||
className="inline-flex items-center gap-1 text-xs font-semibold text-emerald-400 bg-emerald-500/10 px-2.5 py-1.5 rounded-lg border border-emerald-500/20 cursor-default"
|
||||
title="Approved budget is immutable. Status cannot be modified."
|
||||
>
|
||||
<CheckCircle className="w-3 h-3" />
|
||||
<span>Approve</span>
|
||||
</button>
|
||||
)}
|
||||
{b.status !== ApprovalStatus.Approved && canApprove('grant_financials') && isMaker(b.createdBy) && (
|
||||
<span className="text-[10px] text-amber-400 font-mono italic px-1.5 py-0.5 bg-amber-500/10 border border-amber-500/20 rounded">
|
||||
Maker-Checker
|
||||
<Lock className="w-3 h-3 text-emerald-400" />
|
||||
<span>Approved (Immutable)</span>
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setEditingBudget(b);
|
||||
setSelectedBudgetStatus(b.status ?? ApprovalStatus.Pending);
|
||||
setIsEditBudgetModalOpen(true);
|
||||
}}
|
||||
className="inline-flex items-center gap-1 text-xs font-semibold text-slate-200 hover:text-white bg-slate-900/80 hover:bg-slate-800 border border-slate-700/80 px-2.5 py-1.5 rounded-lg transition shadow-xs cursor-pointer"
|
||||
title="Update Budget Status"
|
||||
>
|
||||
<Edit2 className="w-3 h-3" />
|
||||
<span>Update</span>
|
||||
</button>
|
||||
{canApproveRecord('grant_financials', b.createdBy) && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={updateBudgetMutation.isPending}
|
||||
onClick={() => {
|
||||
updateBudgetMutation.mutate({
|
||||
budgetId: b.id,
|
||||
status: ApprovalStatus.Approved,
|
||||
});
|
||||
}}
|
||||
className="inline-flex items-center gap-1 text-xs font-semibold text-emerald-300 hover:text-white bg-emerald-500/10 hover:bg-emerald-500/20 border border-emerald-500/30 px-2.5 py-1.5 rounded-lg transition shadow-xs cursor-pointer"
|
||||
title="Approve Budget Line"
|
||||
>
|
||||
<CheckCircle className="w-3 h-3" />
|
||||
<span>Approve</span>
|
||||
</button>
|
||||
)}
|
||||
{canApprove('grant_financials') && isMaker(b.createdBy) && (
|
||||
<span className="text-[10px] text-amber-400 font-mono italic px-1.5 py-0.5 bg-amber-500/10 border border-amber-500/20 rounded">
|
||||
Maker-Checker
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
@@ -1455,6 +1534,140 @@ export const GrantsPage: React.FC = () => {
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
{/* Modal: Edit Award */}
|
||||
<Modal
|
||||
isOpen={isEditAwardModalOpen}
|
||||
onClose={() => {
|
||||
setIsEditAwardModalOpen(false);
|
||||
setEditingAward(null);
|
||||
}}
|
||||
title={`Edit Award: ${editingAward?.referenceNo || `#${editingAward?.id}`}`}
|
||||
subtitle="Approved funding award record"
|
||||
helpText="Modify the funding programme, recipient organisation, and approved award allocation."
|
||||
>
|
||||
<form
|
||||
onSubmit={editAwardForm.handleSubmit((values) => {
|
||||
if (!editingAward) return;
|
||||
if (Number(editingAward.status) === ApprovalStatus.Approved) {
|
||||
toastError('Record Immutable', 'Approved awards cannot be modified.');
|
||||
return;
|
||||
}
|
||||
updateAwardMutation.mutate({
|
||||
awardId: editingAward.id,
|
||||
organisationId: values.organisationId,
|
||||
programmeId: values.programmeId,
|
||||
amount: values.amount,
|
||||
status: selectedAwardStatus,
|
||||
});
|
||||
})}
|
||||
className="space-y-4"
|
||||
>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
||||
Funding Programme <span className="text-rose-400">*</span>
|
||||
</label>
|
||||
<select
|
||||
{...editAwardForm.register('programmeId')}
|
||||
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 font-medium [&>option]:bg-slate-900 [&>option]:text-white"
|
||||
>
|
||||
<option value="">Select Programme</option>
|
||||
{programmes.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{editAwardForm.formState.errors.programmeId && (
|
||||
<p className="text-[11px] text-rose-400 mt-1">
|
||||
{editAwardForm.formState.errors.programmeId.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
||||
Recipient Organisation <span className="text-rose-400">*</span>
|
||||
</label>
|
||||
<select
|
||||
{...editAwardForm.register('organisationId')}
|
||||
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 font-medium [&>option]:bg-slate-900 [&>option]:text-white"
|
||||
>
|
||||
<option value="">Select Recipient Organisation</option>
|
||||
{organisations.map((o) => (
|
||||
<option key={o.id} value={o.id}>
|
||||
{o.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{editAwardForm.formState.errors.organisationId && (
|
||||
<p className="text-[11px] text-rose-400 mt-1">
|
||||
{editAwardForm.formState.errors.organisationId.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
||||
Approved Award Amount (ZAR) <span className="text-rose-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
{...editAwardForm.register('amount')}
|
||||
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 font-mono"
|
||||
/>
|
||||
{editAwardForm.formState.errors.amount && (
|
||||
<p className="text-[11px] text-rose-400 mt-1">
|
||||
{editAwardForm.formState.errors.amount.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1">
|
||||
Award Lifecycle Status <span className="text-rose-400">*</span>
|
||||
</label>
|
||||
{Number(editingAward?.status) === ApprovalStatus.Approved ? (
|
||||
<div className="w-full px-3 py-2 text-xs border border-emerald-500/30 bg-emerald-500/10 rounded-xl text-emerald-300 font-semibold flex items-center gap-1.5 cursor-default">
|
||||
<Lock className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span>Approved (Immutable - Cannot Be Changed)</span>
|
||||
</div>
|
||||
) : (
|
||||
<select
|
||||
value={selectedAwardStatus}
|
||||
onChange={(e) => setSelectedAwardStatus(Number(e.target.value) as ApprovalStatus)}
|
||||
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 font-medium [&>option]:bg-slate-900 [&>option]:text-white"
|
||||
>
|
||||
<option value={ApprovalStatus.Pending}>Pending Approval</option>
|
||||
<option value={ApprovalStatus.Review}>Under Review</option>
|
||||
<option value={ApprovalStatus.Approved}>Approved & Activated</option>
|
||||
<option value={ApprovalStatus.Rejected}>Rejected</option>
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4 border-t border-slate-800">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsEditAwardModalOpen(false);
|
||||
setEditingAward(null);
|
||||
}}
|
||||
className="px-4 py-2 text-xs font-semibold text-slate-300 hover:text-white bg-slate-800 hover:bg-slate-700 rounded-xl transition cursor-pointer"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={updateAwardMutation.isPending || Number(editingAward?.status) === ApprovalStatus.Approved}
|
||||
className="px-4 py-2 text-xs font-semibold text-white bg-blue-600 hover:bg-blue-500 rounded-xl transition shadow-md shadow-blue-500/20 disabled:opacity-50 cursor-pointer"
|
||||
>
|
||||
{updateAwardMutation.isPending ? 'Saving...' : 'Save Changes'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
{/* Modal: Add Budget */}
|
||||
<Modal
|
||||
isOpen={isBudgetModalOpen}
|
||||
@@ -1463,7 +1676,7 @@ export const GrantsPage: React.FC = () => {
|
||||
subtitle="Maintain approved expenditure breakdown and commitments"
|
||||
helpText="Establishes financial lines for project cost categories. Ensures all disbursements are mapped to audited budget ceilings."
|
||||
>
|
||||
<form onSubmit={budgetForm.handleSubmit((data) => budgetMutation.mutate(data as any))} className="space-y-4">
|
||||
<form onSubmit={budgetForm.handleSubmit((data) => budgetMutation.mutate(data as CreateBudgetRequest))} className="space-y-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-1 mb-1.5">
|
||||
<label className="block text-xs font-semibold text-slate-300">Associated Project *</label>
|
||||
@@ -1567,29 +1780,36 @@ export const GrantsPage: React.FC = () => {
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
|
||||
Approval & Compliance Status *
|
||||
</label>
|
||||
<select
|
||||
value={selectedBudgetStatus}
|
||||
onChange={(e) => setSelectedBudgetStatus(Number(e.target.value) as ApprovalStatus)}
|
||||
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 font-medium [&>option]:bg-slate-900 [&>option]:text-white"
|
||||
>
|
||||
{(() => {
|
||||
const canApproveBudget = canApproveRecord('grant_financials', editingBudget?.createdBy);
|
||||
const isSelf = isMaker(editingBudget?.createdBy);
|
||||
let reason = '';
|
||||
if (!canApprove('grant_financials')) {
|
||||
reason = ' (Requires Update Permission)';
|
||||
} else if (isSelf) {
|
||||
reason = ' (Maker-Checker: Cannot approve own record)';
|
||||
}
|
||||
return (
|
||||
<option value={ApprovalStatus.Approved} disabled={!canApproveBudget}>
|
||||
Approved {reason}
|
||||
</option>
|
||||
);
|
||||
})()}
|
||||
<option value={ApprovalStatus.Pending}>Pending Review</option>
|
||||
<option value={ApprovalStatus.Rejected}>Rejected / Blocked</option>
|
||||
</select>
|
||||
{Number(editingBudget?.status) === ApprovalStatus.Approved ? (
|
||||
<div className="w-full px-3 py-2 text-xs border border-emerald-500/30 bg-emerald-500/10 rounded-xl text-emerald-300 font-semibold flex items-center gap-1.5 cursor-default">
|
||||
<Lock className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span>Approved (Immutable - Status Cannot Be Changed)</span>
|
||||
</div>
|
||||
) : (
|
||||
<select
|
||||
value={selectedBudgetStatus}
|
||||
onChange={(e) => setSelectedBudgetStatus(Number(e.target.value) as ApprovalStatus)}
|
||||
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 font-medium [&>option]:bg-slate-900 [&>option]:text-white"
|
||||
>
|
||||
{(() => {
|
||||
const canApproveBudget = canApproveRecord('grant_financials', editingBudget?.createdBy);
|
||||
const isSelf = isMaker(editingBudget?.createdBy);
|
||||
let reason = '';
|
||||
if (!canApprove('grant_financials')) {
|
||||
reason = ' (Requires Update Permission)';
|
||||
} else if (isSelf) {
|
||||
reason = ' (Maker-Checker: Cannot approve own record)';
|
||||
}
|
||||
return (
|
||||
<option value={ApprovalStatus.Approved} disabled={!canApproveBudget}>
|
||||
Approved {reason}
|
||||
</option>
|
||||
);
|
||||
})()}
|
||||
<option value={ApprovalStatus.Pending}>Pending Review</option>
|
||||
<option value={ApprovalStatus.Rejected}>Rejected / Blocked</option>
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="pt-3 border-t border-slate-800 flex justify-end gap-2">
|
||||
@@ -1605,7 +1825,7 @@ export const GrantsPage: React.FC = () => {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={updateBudgetMutation.isPending || !editingBudget}
|
||||
disabled={updateBudgetMutation.isPending || !editingBudget || Number(editingBudget.status) === ApprovalStatus.Approved}
|
||||
onClick={() => {
|
||||
if (editingBudget) {
|
||||
updateBudgetMutation.mutate({
|
||||
|
||||
Reference in New Issue
Block a user