Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds agreement feedback component #1007

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
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
12 changes: 12 additions & 0 deletions _queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,18 @@ export const useAnonymousVippsAgreement = (agreementUrlCode: string) => {
};
};

export const useAgreementFeedbackTypes = () => {
const { data, error, isValidating } = useSWR(`/agreementfeedback/types`, (url) => fetcher(url));
const loading = !data && !error;

return {
loading,
isValidating,
data,
error,
};
};

export const useOrganizations = (fetchToken: getAccessTokenSilently) => {
const { data, error, isValidating } = useSWR(`/organizations/active/`, (url) =>
fetcher(url, fetchToken),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 1rem;
font-size: 0.8rem;
padding-bottom: 2rem;
}

@media only screen and (max-width: 1180px) {
.grid {
grid-template-columns: 1fr;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import { useRef, useState } from "react";
import { useAgreementFeedbackTypes } from "../../../../_queries";
import {
EffektButton,
EffektButtonVariant,
} from "../../../shared/components/EffektButton/EffektButton";
import { AgreementTypes } from "../../shared/lists/agreementList/AgreementList";
import styles from "./StoppedAgreementFeedback.module.scss";
import {
addStoppedAgreementFeedback,
deleteStoppedAgreementFeedback,
} from "../../shared/lists/agreementList/_queries";
import { useAuth0 } from "@auth0/auth0-react";
import { toast } from "react-toastify";
import { AlertCircle } from "react-feather";

interface FeedbackSelection {
feedbackTypeId: number;
backendId: string | undefined;
}

type OperationType = "add" | "delete";

interface QueuedOperation {
feedbackTypeId: number;
type: OperationType;
backendId?: string; // For delete operations
timestamp: number;
}

export const StoppedAgreementFeedback = ({
agreementId,
KID,
agreementType,
}: {
agreementId: string;
KID: string;
agreementType: AgreementTypes;
}) => {
const { loading, isValidating, data, error } = useAgreementFeedbackTypes();
const { getAccessTokenSilently } = useAuth0();

const [selectedFeedback, setSelectedFeedback] = useState<FeedbackSelection[]>([]);
const operationQueue = useRef<QueuedOperation[]>([]);
const processingQueue = useRef<boolean>(false);

const addToQueue = (operation: QueuedOperation) => {
operationQueue.current.push(operation);
processQueue();
};

const processQueue = async () => {
if (processingQueue.current || operationQueue.current.length === 0) {
return;
}

processingQueue.current = true;
const operation = operationQueue.current[0];

try {
const token = await getAccessTokenSilently();

if (operation.type === "add") {
const insertedId = await addStoppedAgreementFeedback(
agreementId,
KID,
agreementType,
operation.feedbackTypeId,
token,
);

// Find the latest operation for this feedbackTypeId
const latestOp = [...operationQueue.current]
.reverse()
.find((op) => op.feedbackTypeId === operation.feedbackTypeId);

if (latestOp?.type === "add") {
// If latest operation is also an add, we can remove all intermediate operations
// for this feedbackTypeId and update the UI
operationQueue.current = operationQueue.current.filter(
(op) => op.feedbackTypeId !== operation.feedbackTypeId || op === latestOp,
);

setSelectedFeedback((prev) => {
const existing = prev.find((f) => f.feedbackTypeId === operation.feedbackTypeId);
if (existing) {
return prev.map((f) =>
f.feedbackTypeId === operation.feedbackTypeId ? { ...f, backendId: insertedId } : f,
);
}
return [...prev, { feedbackTypeId: operation.feedbackTypeId, backendId: insertedId }];
});
} else {
// Latest operation is delete, so we need to delete this record
await deleteStoppedAgreementFeedback(insertedId, KID, token);
}
} else {
// delete operation
if (operation.backendId) {
await deleteStoppedAgreementFeedback(operation.backendId, KID, token);
}

// Find the latest operation for this feedbackTypeId
const latestOp = [...operationQueue.current]
.reverse()
.find((op) => op.feedbackTypeId === operation.feedbackTypeId);

if (latestOp?.type === "delete") {
// If latest operation is also a delete, we can remove all intermediate operations
// for this feedbackTypeId and update the UI
operationQueue.current = operationQueue.current.filter(
(op) => op.feedbackTypeId !== operation.feedbackTypeId || op === latestOp,
);

setSelectedFeedback((prev) =>
prev.filter((f) => f.feedbackTypeId !== operation.feedbackTypeId),
);
}
}
} catch (error) {
// On error, we only show a toast if this is the latest operation for this feedbackTypeId
const latestOp = [...operationQueue.current]
.reverse()
.find((op) => op.feedbackTypeId === operation.feedbackTypeId);

if (operation === latestOp) {
failureToast(
`Kunne ikke ${operation.type === "add" ? "legge til" : "slette"} tilbakemelding`,
);

// Revert UI state
if (operation.type === "add") {
setSelectedFeedback((prev) =>
prev.filter((f) => f.feedbackTypeId !== operation.feedbackTypeId),
);
} else {
if (operation.backendId) {
setSelectedFeedback((prev) => [
...prev,
{
feedbackTypeId: operation.feedbackTypeId,
backendId: operation.backendId,
},
]);
}
}
}
} finally {
// Remove the processed operation
operationQueue.current = operationQueue.current.slice(1);
processingQueue.current = false;

// Process next operation if any
processQueue();
}
};

const handleFeedbackToggle = (feedbackTypeId: number) => {
const existingSelection = selectedFeedback.find((f) => f.feedbackTypeId === feedbackTypeId);

if (existingSelection) {
// Queue delete operation
addToQueue({
feedbackTypeId,
type: "delete",
backendId: existingSelection.backendId ?? undefined,
timestamp: Date.now(),
});

// Optimistically update UI
setSelectedFeedback((prev) => prev.filter((f) => f.feedbackTypeId !== feedbackTypeId));
} else {
// Queue add operation
addToQueue({
feedbackTypeId,
type: "add",
timestamp: Date.now(),
});

// Optimistically update UI
setSelectedFeedback((prev) => [
...prev,
{
feedbackTypeId,
backendId: undefined,
},
]);
}
};

if (loading) {
return <div>Loading...</div>;
}

if (error) {
return <div>Error: {error.message}</div>;
}

if (!data) {
return <div>No data</div>;
}

return (
<div>
<div>
<h5>Avtale avsluttet</h5>
<p>
Vi har avsluttet din avtale. Det er nyttig for oss om du ønsker å si noe om hvorfor du
avslutter din avtale, slik at vi kan utvikle oss videre.
</p>
<div className={styles.grid}>
{data.map((feedbackType: { ID: number; name: string }) => {
return (
<EffektButton
key={feedbackType.ID}
squared
onClick={() => handleFeedbackToggle(feedbackType.ID)}
selected={selectedFeedback.some((f) => f.feedbackTypeId === feedbackType.ID)}
variant={EffektButtonVariant.SECONDARY}
>
{feedbackType.name}
</EffektButton>
);
})}
</div>
</div>
</div>
);
};

const failureToast = (msg: string) =>
toast.error(msg, { icon: <AlertCircle size={24} color={"black"} /> });
24 changes: 20 additions & 4 deletions components/profile/shared/lists/agreementList/AgreementDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
AgreementMultipleCauseAreaDetailsConfiguration,
} from "./multipleCauseAreasDetails/AgreementMultipleCauseAreasDetails";
import { DatePickerInputConfiguration } from "../../../../shared/components/DatePicker/DatePickerInput";
import { AgreementTypes } from "./AgreementList";

export type AgreementDetailsConfiguration = {
save_button_text: string;
Expand All @@ -52,14 +53,28 @@ export type AgreementDetailsConfiguration = {
};

export const AgreementDetails: React.FC<{
type: "Vipps" | "AvtaleGiro" | "AutoGiro";
type: AgreementTypes;
agreementId: string;
agreementKid: string;
inputSum: number;
inputDate: number;
inputDistribution: Distribution;
taxUnits: TaxUnit[];
endpoint: string;
agreementCancelled: (type: AgreementTypes, agreementId: string, agreementKid: string) => void;
configuration: AgreementDetailsConfiguration;
}> = ({ type, inputSum, inputDate, inputDistribution, taxUnits, endpoint, configuration }) => {
}> = ({
type,
agreementId,
agreementKid,
inputSum,
inputDate,
inputDistribution,
taxUnits,
endpoint,
agreementCancelled,
configuration,
}) => {
const { getAccessTokenSilently, user } = useAuth0();
const { mutate } = useSWRConfig();
// Parse and stringify to make a deep copy of the object
Expand Down Expand Up @@ -220,6 +235,7 @@ export const AgreementDetails: React.FC<{
const cancelled = await cancelVippsAgreement(endpoint, token);
if (cancelled) {
successToast(configuration.toasts_configuration.success_text);
agreementCancelled(type, agreementId, agreementKid);
mutate(`/donors/${getUserId(user)}/recurring/vipps/`);
} else {
failureToast(configuration.toasts_configuration.failure_text);
Expand All @@ -228,6 +244,7 @@ export const AgreementDetails: React.FC<{
const cancelled = await cancelAvtaleGiroAgreement(endpoint, token);
if (cancelled) {
successToast(configuration.toasts_configuration.success_text);
agreementCancelled(type, agreementId, agreementKid);
mutate(`/donors/${getUserId(user)}/recurring/avtalegiro/`);
} else {
failureToast(configuration.toasts_configuration.failure_text);
Expand All @@ -236,6 +253,7 @@ export const AgreementDetails: React.FC<{
const cancelled = await cancelAutoGiroAgreement(endpoint, token);
if (cancelled) {
successToast(configuration.toasts_configuration.success_text);
agreementCancelled(type, agreementId, agreementKid);
mutate(`/donors/${getUserId(user)}/recurring/autogiro/`);
} else {
failureToast(configuration.toasts_configuration.failure_text);
Expand Down Expand Up @@ -333,8 +351,6 @@ export const AgreementDetails: React.FC<{
}
};

const saveAvtaleGiroAgreement = async () => {};

const successToast = (text: string) =>
toast.success(text, { icon: <Check size={24} color={"black"} /> });
const failureToast = (text: string) =>
Expand Down
Loading
Loading