Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 hover:text-primary-900" />
</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"
/>
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