Skip to content
170 changes: 106 additions & 64 deletions frontend/src/components/ActionConfirmation.tsx
Original file line number Diff line number Diff line change
@@ -1,81 +1,123 @@
import Button from "../components/Button";
import Button from "./Button";
import { IoIosWarning } from "react-icons/io";
import { FaCheckCircle, FaInfoCircle } from "react-icons/fa";

{/* The popup that appears on delete */}
const ActionConfirmation = ({
isOpen,
onCloseDelete,
onConfirmDelete,
title,
subtitle = "Are you sure?",
boldSubtitle = "",
warningMessage = "This action cannot be undone."
}: {
isOpen: boolean;
onCloseDelete: () => void;
onConfirmDelete: () => void;
title: string;
subtitle: string;
boldSubtitle : string;
warningMessage: string;
}) => {
if (!isOpen) return null;
export type ActionConfirmationVariant = "create" | "update" | "delete";

return (
<div
className=" fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[1500] transition-opacity duration-300"
onClick={onCloseDelete}
>
<div
className=" bg-white rounded-md shadow-2xl p-8 max-w-xl w-full mx-4 transform transition-all duration-300"
onClick={(e) => e.stopPropagation()}
>

{/* Title */}
<h3 className="text-2xl font-bold text-black text-center mb-2">
{title}
</h3>
const ActionConfirmation = ({
isOpen,
onCloseDelete,
onConfirmDelete,
title,
subtitle = "Are you sure?",
boldSubtitle = "",
warningMessage = "This action cannot be undone.",
variant = "delete",
}: {
isOpen: boolean;
onCloseDelete: () => void;
onConfirmDelete: () => void;
title: string;
subtitle: string;
boldSubtitle: string;
warningMessage: string;
variant?: ActionConfirmationVariant;
}) => {
if (!isOpen) return null;

{/* Message */}
<p className="text-gray-600 text-center mb-6 text-lg">
{subtitle + " "}
<span className="font-bold">{boldSubtitle}</span>
{"?"}
</p>
const styles =
variant === "create"
? {
panel: "border-t-4 border-green bg-green-light/30",
stripe: "bg-green",
box: "bg-green-light",
Icon: FaCheckCircle,
iconClass: "text-green",
label: "Confirm",
labelClass: "text-green",
textClass: "text-green-dark",
cancelClass:
"text-grey-700 border-grey-500 hover:border-grey-600 hover:bg-grey-150 active:bg-grey-200",
}
: variant === "update"
? {
panel: "border-t-4 border-grey-400 bg-grey-150",
stripe: "bg-grey-500",
box: "bg-grey-200",
Icon: FaInfoCircle,
iconClass: "text-grey-700",
label: "Review",
labelClass: "text-grey-800",
textClass: "text-grey-800",
cancelClass:
"text-grey-700 border-grey-500 hover:border-grey-600 hover:bg-grey-200 active:bg-grey-300",
}
: {
panel: "border-t-4 border-red bg-red-lightest/40",
stripe: "bg-red",
box: "bg-red-light",
Icon: IoIosWarning,
iconClass: "text-red",
label: "Warning",
labelClass: "text-red",
textClass: "text-red",
cancelClass:
"text-red border-red hover:border-red hover:bg-red-light active:bg-red",
};

<div className="max-w-md mx-auto ">
const { Icon } = styles;

<div className="flex mb-6">
return (
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[1500] transition-opacity duration-300"
onClick={onCloseDelete}
>
<div
className={`rounded-md shadow-2xl p-8 max-w-xl w-full mx-4 transform transition-all duration-300 !bg-white ${styles.panel}`}
onClick={(e) => e.stopPropagation()}
>
<h3 className="text-2xl font-bold text-black text-center mb-2">{title}</h3>

<div className="w-3 bg-red"/>
<div className="p-3 bg-red-light">
<div className="flex">
<IoIosWarning size={24} className="text-red"/>
<p className="font-bold px-1 text-lg text-red"> Warning </p>
</div>
<p className=" text-left font-semibold text-red">
{warningMessage}
</p>
<p className="text-gray-600 text-center mb-6 text-lg">
{subtitle + " "}
<span className="font-bold">{boldSubtitle}</span>
{"?"}
</p>

<div className="max-w-md mx-auto ">
<div className="flex mb-6">
<div className={`w-3 shrink-0 ${styles.stripe}`} />
<div className={`p-3 flex-1 min-w-0 ${styles.box}`}>
<div className="flex items-center">
<Icon size={24} className={`shrink-0 ${styles.iconClass}`} />
<p className={`font-bold px-1 text-lg ${styles.labelClass}`}>
{styles.label}
</p>
</div>
<p className={`text-left font-semibold ${styles.textClass}`}>
{warningMessage}
</p>
</div>

</div>


{/* Buttons */}
<div className="flex w-full justify-between ">
<Button text="No, cancel" onClick={onCloseDelete} className=" text-white bg-red hover:border-red hover:text-red hover:bg-white active:bg-red" />
<Button text="Yes, confirm" onClick={() => {
<Button
text="No, cancel"
onClick={onCloseDelete}
className={styles.cancelClass}
/>
<Button
text="Yes, confirm"
onClick={() => {
onConfirmDelete();
onCloseDelete();
}} className="border-grey-500" />
</div>

}}
className="border-grey-500"
/>
</div>

</div>
</div>
);
};
</div>
);
};

export default ActionConfirmation;
export default ActionConfirmation;
32 changes: 30 additions & 2 deletions frontend/src/main-page/cash-flow/components/CashAddRevenue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import CashRevenueInstallment, {
EditableInstallment,
} from "./CashRevenueInstallment";
import { createNewRevenue, isValidInstallment, toInstallment } from "../../cash-flow/processCashflowDataEditSave";
import ActionConfirmation from "../../../components/ActionConfirmation";

type FieldErrors = {
type?: string;
Expand Down Expand Up @@ -46,6 +47,10 @@ export default function CashAddRevenue() {
const [errors, setErrors] = useState<FieldErrors>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [showConfirmModal, setShowConfirmModal] = useState(false);
const [pendingRevenue, setPendingRevenue] = useState<CashflowRevenue | null>(
null,
);

const showSuccessMessage = (message: string) => {
setSuccessMessage(message);
Expand Down Expand Up @@ -120,12 +125,19 @@ export default function CashAddRevenue() {
setErrors({});
}

const handleSubmit = async () => {
const requestSubmit = () => {
setSuccessMessage(null);
const payload = buildPayload();
if (!payload) {
return;
}
setPendingRevenue(payload);
setShowConfirmModal(true);
};

const handleConfirmedSubmit = async () => {
if (!pendingRevenue) return;
const payload = pendingRevenue;

setIsSubmitting(true);
setErrors((previous) => ({ ...previous, submit: undefined }));
Expand Down Expand Up @@ -192,6 +204,22 @@ export default function CashAddRevenue() {

return (
<div className="flex flex-col pt-2 px-2 col-span-2 h-full gap-2">
<ActionConfirmation
isOpen={showConfirmModal}
onCloseDelete={() => {
setShowConfirmModal(false);
setPendingRevenue(null);
}}
onConfirmDelete={() => {
void handleConfirmedSubmit();
setPendingRevenue(null);
}}
title="Create revenue source"
subtitle="Are you sure you want to add"
boldSubtitle={pendingRevenue?.name ?? ""}
warningMessage="This will create a new revenue line in your cash flow."
variant="create"
/>
<div className="text-lg lg:text-xl w-full text-left font-bold">
{"Add Revenue Source"}
</div>
Expand Down Expand Up @@ -297,7 +325,7 @@ export default function CashAddRevenue() {
/>
<Button
text="Add Revenue Source"
onClick={handleSubmit}
onClick={requestSubmit}
disabled={isSubmitting}
className="bg-green hover:!border-green text-white mt-2 text-sm lg:text-base active:!bg-green active:!border-green w-full"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export default function CashEditLineItem({
subtitle={"Are you sure you want to delete"}
boldSubtitle={sourceName}
warningMessage="If you delete this item, it will be permanently removed from the system."
variant="delete"
/>
</div>
);
Expand Down
36 changes: 31 additions & 5 deletions frontend/src/main-page/cash-flow/components/CashEditRevenue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import { Installment } from "../../../../../middle-layer/types/Installment";
import { RevenueType } from "../../../../../middle-layer/types/RevenueType";
import Button from "../../../components/Button";
import InputField from "../../../components/InputField";
import {
saveRevenueEdits,
} from "../processCashflowDataEditSave";
import { saveRevenueEdits } from "../processCashflowDataEditSave";
import ActionConfirmation from "../../../components/ActionConfirmation";
import CashCategoryDropdown from "./CashCategoryDropdown";
import CashRevenueInstallment, {
EditableInstallment,
Expand Down Expand Up @@ -71,6 +70,10 @@ export default function CashEditRevenue({
);
const [errors, setErrors] = useState<FieldErrors>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [showConfirmModal, setShowConfirmModal] = useState(false);
const [pendingRevenue, setPendingRevenue] = useState<CashflowRevenue | null>(
null,
);

const isValidInstallment = (installment: EditableInstallment) => {
if (installment.amount === null || installment.date === null) {
Expand Down Expand Up @@ -196,11 +199,18 @@ export default function CashEditRevenue({
)
: (singleInstallment.amount ?? 0);

const handleSave = async () => {
const requestSave = () => {
const payload = buildPayload();
if (!payload) {
return;
}
setPendingRevenue(payload);
setShowConfirmModal(true);
};

const handleConfirmedSave = async () => {
if (!pendingRevenue) return;
const payload = pendingRevenue;

setIsSubmitting(true);
setErrors((previous) => ({ ...previous, submit: undefined }));
Expand All @@ -221,6 +231,22 @@ export default function CashEditRevenue({

return (
<div className="flex flex-col w-full gap-4">
<ActionConfirmation
isOpen={showConfirmModal}
onCloseDelete={() => {
setShowConfirmModal(false);
setPendingRevenue(null);
}}
onConfirmDelete={() => {
void handleConfirmedSave();
setPendingRevenue(null);
}}
title="Update revenue source"
subtitle="Are you sure you want to save changes to"
boldSubtitle={pendingRevenue?.name ?? revenueItem.name}
warningMessage="This will update this revenue line in your cash flow."
variant="update"
/>
<div className="grid grid-cols-1 xl:grid-cols-2 w-full gap-4">
<div className="flex flex-col gap-1">
<InputField
Expand Down Expand Up @@ -333,7 +359,7 @@ export default function CashEditRevenue({
/>
<Button
text={isSubmitting ? "Saving..." : "Save"}
onClick={handleSave}
onClick={requestSave}
disabled={isSubmitting}
className="bg-primary-900 text-white text-sm lg:text-base"
/>
Expand Down
24 changes: 23 additions & 1 deletion frontend/src/main-page/grants/edit-grant/EditGrant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const EditGrant: React.FC<{
// State to track if form was submitted successfully
const [saving, setSaving] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showSaveModal, setShowSaveModal] = useState(false);

const [form, dispatch] = useReducer(reducer, {
organization: grantToEdit?.organization ?? "",
Expand Down Expand Up @@ -187,7 +188,7 @@ const EditGrant: React.FC<{
<Button
text="Save"
className="bg-primary-900 text-white px-3 py-1"
onClick={handleSubmit}
onClick={() => setShowSaveModal(true)}
disabled={saving}
/>
</div>
Expand Down Expand Up @@ -222,8 +223,29 @@ const EditGrant: React.FC<{
subtitle={"Are you sure you want to delete"}
boldSubtitle={form.organization}
warningMessage="If you delete this grant, it will be permanently removed from the system."
variant="delete"
/>
</div>)}
<ActionConfirmation
isOpen={showSaveModal}
onCloseDelete={() => setShowSaveModal(false)}
onConfirmDelete={() => {
handleSubmit();
}}
title={grantToEdit ? "Save Grant" : "Create Grant"}
subtitle={
grantToEdit
? "Are you sure you want to save changes to"
: "Are you sure you want to create a grant for"
}
boldSubtitle={form.organization}
warningMessage={
grantToEdit
? "Saving will update this grant's details in the system."
: "A new grant will be added to the system with these details."
}
variant={grantToEdit ? "update" : "create"}
/>
</div>
</div>
{/* Error Popup */}
Expand Down
Loading
Loading