Upload files to "src/components"

This commit is contained in:
gitohn 2025-03-16 10:13:52 +00:00
parent 91c29219c2
commit 91186b4a8a
5 changed files with 671 additions and 1 deletions

View File

@ -1 +1,78 @@
aa
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useMessageListener } from "@/hooks/useMessageListener";
import * as Notification from "@/Utils/Notifications";
import useQuery from "@/Utils/request/useQuery";
import { AdditionalDischargeProceduresComponentType } from "@/pluginTypes";
import routes from "../api";
import { HCXClaimModel } from "../types";
import ClaimCard from "./ClaimCard";
import CreateClaimCard from "./CreateClaimCard";
const AdditionalDischargeProcedures: AdditionalDischargeProceduresComponentType =
({ consultation }) => {
const { t } = useTranslation();
const [latestClaim, setLatestClaim] = useState<HCXClaimModel>();
const [isCreateClaimLoading, setIsCreateClaimLoading] = useState(false);
const { refetch: refetchLatestClaim } = useQuery(routes.hcx.claims.list, {
query: {
consultation: consultation.id,
ordering: "-modified_date",
use: "claim",
outcome: "complete",
limit: 1,
},
onResponse: (res) => {
if (!isCreateClaimLoading) return;
setIsCreateClaimLoading(false);
if (res?.data?.results?.length !== 0) {
setLatestClaim(res?.data?.results[0]);
Notification.Success({
msg: t("claim__fetched_claim_approval_results"),
});
return;
}
setLatestClaim(undefined);
Notification.Success({
msg: t("claim__error_fetching_claim_approval_results"),
});
},
});
useMessageListener((data) => {
if (
data.type === "MESSAGE" &&
(data.from === "claim/on_submit" ||
data.from === "preauth/on_submit") &&
data.message === "success"
) {
refetchLatestClaim();
}
});
return (
<div className="my-5 rounded p-5 shadow">
<h2 className="mb-2">{t("claim_insurance")}</h2>
{latestClaim ? (
<ClaimCard claim={latestClaim} />
) : (
<CreateClaimCard
consultationId={consultation.id ?? ""}
patientId={consultation.patient ?? ""}
use="claim"
isCreating={isCreateClaimLoading}
setIsCreating={setIsCreateClaimLoading}
/>
)}
</div>
);
};
export default AdditionalDischargeProcedures;

View File

@ -0,0 +1,55 @@
import { useLayoutEffect, useRef, useState } from "react";
import CareIcon from "@/CAREUI/icons/CareIcon";
import ClaimCardCommunication from "./ClaimCardCommunication";
import ClaimCardInfo from "./ClaimCardInfo";
import { HCXClaimModel } from "../types";
interface IProps {
claim: HCXClaimModel;
}
export default function ClaimCard({ claim }: IProps) {
const [showMessages, setShowMessages] = useState(false);
const [containerDimensions, setContainerDimensions] = useState({
width: 0,
height: 0,
});
const cardContainerRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
if (cardContainerRef.current) {
setContainerDimensions({
width: cardContainerRef.current.offsetWidth,
height: cardContainerRef.current.offsetHeight,
});
}
}, [cardContainerRef]);
return (
<>
<div className="relative flex justify-end">
<CareIcon
icon={showMessages ? "l-info-circle" : "l-chat"}
className="absolute right-0 top-0 z-30 h-7 w-7 cursor-pointer text-gray-600 hover:text-gray-800"
onClick={() => setShowMessages((prev) => !prev)}
/>
</div>
{showMessages ? (
<div
style={{ ...containerDimensions }}
className="relative w-full px-2 lg:px-8"
>
<ClaimCardCommunication claim={claim} />
</div>
) : (
<div
ref={cardContainerRef}
className="w-full px-2 lg:px-8" // TODO: add a card flip animation
>
<ClaimCardInfo claim={claim} />
</div>
)}
</>
);
}

View File

@ -0,0 +1,304 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import CareIcon from "@/CAREUI/icons/CareIcon";
import ButtonV2 from "@/components/Common/ButtonV2";
import TextAreaFormField from "@/components/Form/FormFields/TextAreaFormField";
import { FileUploadModel } from "@/components/Patient/models";
import useFileUpload from "@/hooks/useFileUpload";
import * as Notification from "@/Utils/Notifications";
import coreRoutes from "@/Utils/request/api";
import request from "@/Utils/request/request";
import useQuery from "@/Utils/request/useQuery";
import { classNames } from "@/Utils/utils";
import routes from "../api";
import { HCXClaimModel, HCXCommunicationModel } from "../types";
interface IProps {
claim: HCXClaimModel;
}
export default function ClaimCardCommunication({ claim }: IProps) {
const { t } = useTranslation();
const [inputText, setInputText] = useState("");
const [isSendingCommunication, setIsSendingCommunication] = useState(false);
const {
Input,
files,
error,
removeFile,
clearFiles,
handleFileUpload,
validateFiles,
} = useFileUpload({
multiple: true,
type: "COMMUNICATION",
allowedExtensions: [".pdf", ".jpg", ".jpeg", ".png"],
});
const { data: communicationsResult, refetch: refetchCommunications } =
useQuery(routes.hcx.communications.list, {
query: {
claim: claim.id,
ordering: "-created_date",
},
});
const handleSubmit = async () => {
if (!claim.id) return;
if (!validateFiles()) return;
setIsSendingCommunication(true);
const { res, data } = await request(routes.hcx.communications.create, {
body: {
claim: claim.id,
content: [
{
type: "text",
data: inputText,
},
],
},
});
if (res?.status === 201 && data) {
await handleFileUpload(data.id as string);
const { res } = await request(routes.hcx.communications.send, {
body: {
communication: data.id,
},
});
if (res?.ok) {
Notification.Success({ msg: t("communication__sent_to_hcx") });
await refetchCommunications();
setInputText("");
clearFiles();
}
}
setIsSendingCommunication(false);
};
return (
<div className="flex h-full !w-full flex-col justify-end">
<CommunicationChatInterface
communications={communicationsResult?.results ?? []}
/>
<div className="flex w-full items-center gap-3 max-md:flex-col">
<div className="relative w-full flex-1">
<div className="absolute bottom-full flex max-w-full items-center gap-2 overflow-x-auto rounded-md bg-white p-2">
{files.map((file, i) => (
<div
key={file.name}
className="flex min-w-36 max-w-36 items-center gap-2"
>
<div>
{file.type.includes("image") ? (
<img
src={URL.createObjectURL(file)}
alt={file.name}
className="h-10 w-10 rounded object-cover"
/>
) : (
<div className="flex h-10 w-10 items-center justify-center rounded bg-gray-300">
<CareIcon icon="l-file" className="h-5 w-5" />
</div>
)}
</div>
<div>
<p className="w-24 truncate text-sm">{file.name}</p>
<div className="flex !items-center gap-2.5">
<p className="text-xs text-gray-500">
{(file.size / 1024).toFixed(2)} KB
</p>
<button
onClick={() => {
removeFile(i);
}}
>
<CareIcon
icon="l-trash"
className="h-4 w-4 text-danger-500"
/>
</button>
</div>
</div>
</div>
))}
</div>
<TextAreaFormField
name="message"
value={inputText}
onChange={(e) => setInputText(e.value)}
placeholder={t("enter_message")}
rows={1}
className="-mb-3 flex-1"
/>
</div>
<div className="flex items-center justify-center max-md:w-full">
<label className="button-size-default button-shape-square button-primary-default inline-flex h-min w-full cursor-pointer items-center justify-center gap-2 whitespace-pre font-medium outline-offset-1 transition-all duration-200 ease-in-out">
<CareIcon icon="l-paperclip" className="h-5 w-5" />
<span className="md:hidden">{t("add_attachments")}</span>
<Input />
</label>
</div>
<ButtonV2
disabled={!inputText}
loading={isSendingCommunication}
onClick={handleSubmit}
className="max-md:w-full"
>
{t("send_message")}
</ButtonV2>
</div>
{error && (
<p className="pt-1.5 text-xs font-medium text-danger-600">{error}</p>
)}
</div>
);
}
interface ICommunicationChatInterfaceProps {
communications: HCXCommunicationModel[];
}
function CommunicationChatInterface({
communications,
}: ICommunicationChatInterfaceProps) {
return (
<div className="my-3 flex h-full w-full flex-col-reverse gap-4 overflow-y-auto">
{communications?.map((communication) => (
<CommunicationChatMessage communication={communication} />
))}
</div>
);
}
interface ICommunicationChatMessageProps {
communication: HCXCommunicationModel;
}
function CommunicationChatMessage({
communication,
}: ICommunicationChatMessageProps) {
const { t } = useTranslation();
const [attachments, setAttachments] = useState<null | FileUploadModel[]>(
null,
);
const [isFetchingAttachments, setIsFetchingAttachments] = useState(false);
const [isDownloadingAttachment, setIsDownloadingAttachment] = useState(false);
return (
<div
className={classNames(
"mb-4 flex flex-col gap-2",
communication.created_by ? "items-end pr-2" : "items-start pl-2",
)}
>
{communication.content?.map((message) => (
<p
className={classNames(
"ml-2 px-4 py-3 text-white",
communication.created_by
? "rounded-bl-3xl rounded-tl-3xl rounded-tr-xl bg-blue-400"
: "rounded-br-3xl rounded-tl-xl rounded-tr-3xl bg-gray-500",
)}
>
{message.data}
</p>
))}
{attachments ? (
<div className="flex max-w-full items-center gap-2 overflow-x-auto pb-2.5">
{attachments.length === 0 ? (
<p className="text-sm text-secondary-600">
{t("no_attachments_found")}
</p>
) : (
attachments.map((attachment) => (
<div
key={attachment.id}
className="flex min-w-36 max-w-36 items-center gap-2"
>
<div>
<div className="flex h-10 w-10 items-center justify-center rounded bg-gray-300">
<CareIcon icon="l-file" className="h-5 w-5" />
</div>
</div>
<div className="flex flex-col items-start gap-1">
<p className="w-24 truncate text-sm">{attachment.name}</p>
<button
disabled={isDownloadingAttachment}
onClick={async () => {
if (!attachment.id) return;
setIsDownloadingAttachment(true);
const { res, data } = await request(
coreRoutes.retrieveUpload,
{
query: {
file_type: "COMMUNICATION",
associating_id: communication.id,
},
pathParams: { id: attachment.id },
},
);
if (res?.ok) {
const url = data?.read_signed_url;
window.open(url, "_blank");
}
setIsDownloadingAttachment(false);
}}
className="cursor-pointer text-xs text-blue-500 hover:text-blue-700 hover:underline"
>
{t("open")}
</button>
</div>
</div>
))
)}
</div>
) : (
<button
onClick={async () => {
setIsFetchingAttachments(true);
const { res, data } = await request(coreRoutes.viewUpload, {
query: {
file_type: "COMMUNICATION",
associating_id: communication.id,
is_archived: false,
},
});
if (res?.ok) {
Notification.Success({
msg: t("fetched_attachments_successfully"),
});
setAttachments(data?.results ?? []);
}
setIsFetchingAttachments(false);
}}
className="cursor-pointer text-sm text-secondary-700 hover:text-secondary-900 hover:underline"
>
{isFetchingAttachments ? t("fetching") + "..." : t("see_attachments")}
</button>
)}
</div>
);
}

View File

@ -0,0 +1,168 @@
import { classNames, formatCurrency, formatDateTime } from "@/Utils/utils";
import { HCXClaimModel } from "../types";
import { useTranslation } from "react-i18next";
interface IProps {
claim: HCXClaimModel;
}
const claimStatus = {
PENDING: "pending",
APPROVED: "approved",
REJECTED: "rejected",
};
export default function ClaimCardInfo({ claim }: IProps) {
const { t } = useTranslation();
const status =
claim.outcome === "Complete"
? claim.error_text
? claimStatus.REJECTED
: claimStatus.APPROVED
: claimStatus.PENDING;
return (
<>
<div className="sm:flex sm:items-end">
<div className="sm:flex-auto">
<h1 className="text-xl font-semibold text-secondary-700">
#{claim.id?.slice(0, 5)}
</h1>
<p className="mt-2 text-sm text-secondary-700">
{t("created_on")}{" "}
<time dateTime={claim.created_date}>
{formatDateTime(claim.created_date ?? "")}
</time>
.
</p>
</div>
<div className="mt-4 flex flex-row-reverse items-center justify-center gap-3 max-sm:justify-end sm:ml-16 sm:mt-0">
{claim.use && (
<span className="rounded bg-primary-100 p-1 px-2 text-sm font-bold text-primary-500 shadow">
{claim.use}
</span>
)}
<span
className={classNames(
"rounded p-1 px-2 text-sm font-bold text-white shadow",
status === claimStatus.APPROVED && "bg-primary-400",
status === claimStatus.REJECTED && "bg-danger-400",
status === claimStatus.PENDING && "bg-yellow-400",
)}
>
{t(`claim__status__${status}`)}
</span>
</div>
</div>
<div className="mt-6 grid gap-4 sm:grid-cols-2">
<div className="text-center">
<h2 className="text-lg font-bold text-secondary-800">
{claim.policy_object?.policy_id || "NA"}
</h2>
<p className="text-sm text-secondary-500">{t("policy__policy_id")}</p>
</div>
<div className="text-center">
<h2 className="text-lg font-bold text-secondary-800">
{claim.policy_object?.subscriber_id || "NA"}
</h2>
<p className="text-sm text-secondary-500">
{t("policy__subscriber_id")}
</p>
</div>
<div className="text-center">
<h2 className="text-lg font-bold text-secondary-800">
{claim.policy_object?.insurer_id?.split("@").shift() || "NA"}
</h2>
<p className="text-sm text-secondary-500">
{t("policy__insurer_id")}
</p>
</div>
<div className="text-center">
<h2 className="text-lg font-bold text-secondary-800">
{claim.policy_object?.insurer_name || "NA"}
</h2>
<p className="text-sm text-secondary-500">
{t("policy__insurer_name")}
</p>
</div>
</div>
<div className="-mx-6 mt-8 flow-root sm:mx-0">
<table className="min-w-full divide-y divide-secondary-300">
<thead>
<tr>
<th
scope="col"
className="py-3.5 pl-6 pr-3 text-left text-sm font-semibold text-secondary-900 sm:pl-0"
>
{t("claim__items")}
</th>
<th></th>
<th></th>
<th
scope="col"
className="py-3.5 pl-3 pr-6 text-right text-sm font-semibold text-secondary-900 sm:pr-0"
>
{t("claim__item__price")}
</th>
</tr>
</thead>
<tbody>
{claim.items?.map((item) => (
<tr key={item.id} className="border-b border-secondary-200">
<td className="py-4 pl-6 pr-3 text-sm sm:pl-0">
<div className="font-medium text-secondary-900">
{item.name}
</div>
<div className="mt-0.5 text-secondary-500">{item.id}</div>
</td>
<td></td>
<td></td>
<td className="py-4 pl-3 pr-6 text-right text-sm text-secondary-500 sm:pr-0">
{formatCurrency(item.price)}
</td>
</tr>
))}
</tbody>
<tfoot>
<tr>
<th
scope="row"
colSpan={3}
className="table-cell pl-6 pr-3 pt-6 text-right text-sm font-normal text-secondary-500 sm:pl-0"
>
{t("claim__total_claim_amount")}
</th>
<td className="pl-3 pr-6 pt-6 text-right text-sm text-secondary-500 sm:pr-0">
{claim.total_claim_amount &&
formatCurrency(claim.total_claim_amount)}
</td>
</tr>
<tr>
<th
scope="row"
colSpan={3}
className="table-cell pl-6 pr-3 pt-4 text-right text-sm font-semibold text-secondary-900 sm:pl-0"
>
{t("claim__total_approved_amount")}
</th>
<td className="pl-3 pr-6 pt-4 text-right text-sm font-semibold text-secondary-900 sm:pr-0">
{claim.total_amount_approved
? formatCurrency(claim.total_amount_approved)
: "NA"}
</td>
</tr>
</tfoot>
</table>
</div>
{claim.error_text && (
<div className="mt-4 text-center text-sm font-bold text-red-500">
{claim.error_text}
</div>
)}
</>
);
}

View File

@ -0,0 +1,66 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import CareIcon from "@/CAREUI/icons/CareIcon";
import { Submit } from "@/components/Common/ButtonV2";
import DialogModal from "@/components/Common/Dialog";
import { FileUpload } from "@/components/Files/FileUpload";
import * as Notification from "@/Utils/Notifications";
import request from "@/Utils/request/request";
import routes from "../api";
import { HCXClaimModel } from "../types";
interface Props {
claim: HCXClaimModel;
show: boolean;
onClose: () => void;
}
export default function ClaimCreatedModal({ claim, ...props }: Props) {
const { t } = useTranslation();
const [isMakingClaim, setIsMakingClaim] = useState(false);
const { use } = claim;
const handleSubmit = async () => {
setIsMakingClaim(true);
const { res } = await request(routes.hcx.claims.makeClaim, {
body: { claim: claim.id },
});
if (res?.ok) {
Notification.Success({ msg: `${use} requested` });
props.onClose();
}
setIsMakingClaim(false);
};
return (
<DialogModal
show={props.show}
onClose={props.onClose}
title={t("add_attachments")}
description={`${t("claim__use__claim")}: #${claim.id?.slice(0, 5)}`}
className="w-full max-w-screen-lg"
titleAction={
<Submit disabled={isMakingClaim} onClick={handleSubmit}>
{isMakingClaim && (
<CareIcon icon="l-spinner" className="animate-spin" />
)}
{isMakingClaim
? t("claim__requesting_claim")
: t("claim__request_claim")}
</Submit>
}
>
<div className="p-4 pt-8">
<FileUpload type="CLAIM" claimId={claim.id} />
</div>
</DialogModal>
);
}