diff --git a/.github/workflows/label-merge-conflict.yml b/.github/workflows/label-merge-conflict.yml
index d538cbae14b..8c0b06e5baa 100644
--- a/.github/workflows/label-merge-conflict.yml
+++ b/.github/workflows/label-merge-conflict.yml
@@ -16,7 +16,7 @@ jobs:
if: github.repository == 'ohcnetwork/care_fe'
runs-on: ubuntu-24.04-arm
steps:
- - uses: prince-chrismc/label-merge-conflicts-action@v2
+ - uses: prince-chrismc/label-merge-conflicts-action@v3
with:
conflict_label_name: "merge conflict"
github_token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 6eb50339b12..9f949652b15 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -11,7 +11,7 @@ permissions:
jobs:
release:
name: Release on Push
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
diff --git a/package-lock.json b/package-lock.json
index 64631cc64f9..d457f3b8f58 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16684,9 +16684,9 @@
}
},
"node_modules/sonner": {
- "version": "1.7.2",
- "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.2.tgz",
- "integrity": "sha512-zMbseqjrOzQD1a93lxahm+qMGxWovdMxBlkTbbnZdNqVLt4j+amF9PQxUCL32WfztOFt9t9ADYkejAL3jF9iNA==",
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.3.tgz",
+ "integrity": "sha512-KXLWQfyR6AHpYZuQk8eO8fCbZSJY3JOpgsu/tbGc++jgPjj8JsR1ZpO8vFhqR/OxvWMQCSAmnSShY0gr4FPqHg==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
diff --git a/public/locale/en.json b/public/locale/en.json
index decf07e84a6..b80f7d59a7b 100644
--- a/public/locale/en.json
+++ b/public/locale/en.json
@@ -949,6 +949,7 @@
"external_identifier": "External Identifier",
"facilities": "Facilities",
"facility": "Facility",
+ "facility_actions_menu": "Facility action menu",
"facility_added_successfully": "Facility created successfully",
"facility_consent_requests_page_title": "Patient Consent List",
"facility_count_one": "{{count}} Facility",
@@ -1294,8 +1295,7 @@
"never_logged_in": "Never Logged In",
"new_password": "New Password",
"new_password_confirmation": "Confirm New Password",
- "new_password_different_from_old": "Your new password is different from the old password.",
- "new_password_same_as_old": "Your new password must not match the old password.",
+ "new_password_same_as_old": "Your new password must not match the old password ",
"new_password_validation": "New password is not valid.",
"new_session": "New Session",
"next_month": "Next month",
@@ -1440,21 +1440,18 @@
"pain_chart_description": "Mark region and intensity of pain",
"passport_number": "Passport Number",
"password": "Password",
- "password_length_met": "It's at least 8 characters long",
- "password_length_validation": "Use at least 8 characters",
- "password_lowercase_met": "It includes at least one lowercase letter",
- "password_lowercase_validation": "Include at least one lowercase letter",
+ "password_length_validation": "Use at least 8 characters",
+ "password_lowercase_validation": "Include at least one lowercase letter (a-z)",
"password_mismatch": "Passwords do not match",
- "password_number_met": "It includes at least one number.",
- "password_number_validation": "Include at least one number.",
+ "password_number_validation": "Include at least one number (0-9)",
"password_required": "Password is required",
"password_reset_failure": "Password Reset Failed",
"password_reset_success": "Password Reset successfully",
"password_sent": "Password Reset Email Sent",
+ "password_success_message": "All set! Your password is strong",
"password_update_error": "Error while updating password. Try again later.",
"password_updated": "Password updated successfully",
- "password_uppercase_met": "It includes at least one uppercase letter.",
- "password_uppercase_validation": "Include at least one uppercase letter.",
+ "password_uppercase_validation": "Include at least one uppercase letter (A-Z).",
"passwords_match": "Passwords match.",
"patient": "Patient",
"patient-notes": "Notes",
@@ -2135,12 +2132,14 @@
"username": "Username",
"username_already_exists": "This username already exists",
"username_available": "Username is available",
- "username_characters_validation": "Only lowercase letters, numbers, and . _ - are allowed",
- "username_consecutive_validation": "Cannot contain consecutive special characters",
- "username_max_length_validation": "Use at most 16 characters",
- "username_min_length_validation": "Use at least 4 characters",
+ "username_characters_validation": "Only lowercase letters, numbers, and . _ - are allowed",
+ "username_consecutive_validation": "Cannot contain consecutive special characters",
+ "username_max_length_validation": "Use at most 16 characters",
+ "username_min_length_validation": "Use at least 4 characters",
"username_not_available": "Username is not available",
- "username_start_end_validation": "Must start and end with a letter or number",
+ "username_not_valid": "username is not valid",
+ "username_start_end_validation": "Must start and end with a letter or number",
+ "username_success_message": "All set! Your username is strong",
"username_userdetails_not_found": "Unable to fetch details as username or user details not found",
"username_valid": "Username is valid",
"users": "Users",
diff --git a/src/components/Auth/ResetPassword.tsx b/src/components/Auth/ResetPassword.tsx
index 273e5486140..1f75e5a1ff6 100644
--- a/src/components/Auth/ResetPassword.tsx
+++ b/src/components/Auth/ResetPassword.tsx
@@ -6,7 +6,7 @@ import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { PasswordInput } from "@/components/ui/input-password";
-import { validateRule } from "@/components/Users/UserFormValidations";
+import { ValidationHelper } from "@/components/Users/UserFormValidations";
import { LocalStorageKeys } from "@/common/constants";
import { validatePassword } from "@/common/validation";
@@ -27,9 +27,7 @@ const ResetPassword = (props: ResetPasswordProps) => {
const initErr: any = {};
const [form, setForm] = useState(initForm);
const [errors, setErrors] = useState(initErr);
- const [passwordInputInFocus, setPasswordInputInFocus] = useState(false);
- const [confirmPasswordInputInFocus, setConfirmPasswordInputInFocus] =
- useState(false);
+ const [isPasswordFieldFocused, setIsPasswordFieldFocused] = useState(false);
const { t } = useTranslation();
const handleChange = (e: any) => {
@@ -124,40 +122,41 @@ const ResetPassword = (props: ResetPasswordProps) => {
name="password"
placeholder={t("new_password")}
onChange={handleChange}
- onFocus={() => setPasswordInputInFocus(true)}
- onBlur={() => setPasswordInputInFocus(false)}
+ onFocus={() => setIsPasswordFieldFocused(true)}
+ onBlur={() => setIsPasswordFieldFocused(false)}
/>
{errors.password && (
{errors.password}
)}
- {passwordInputInFocus && (
-
- {validateRule(
- form.password?.length >= 8,
- t("password_length_validation"),
- !form.password,
- t("password_length_met"),
- )}
- {validateRule(
- form.password !== form.password.toUpperCase(),
- t("password_lowercase_validation"),
- !form.password,
- t("password_lowercase_met"),
- )}
- {validateRule(
- form.password !== form.password.toLowerCase(),
- t("password_uppercase_validation"),
- !form.password,
- t("password_uppercase_met"),
- )}
- {validateRule(
- /\d/.test(form.password),
- t("password_number_validation"),
- !form.password,
- t("password_number_met"),
- )}
+ {isPasswordFieldFocused && (
+
+ = 8,
+ },
+ {
+ description: "password_lowercase_validation",
+ fulfilled: /[a-z]/.test(form.password),
+ },
+ {
+ description: "password_uppercase_validation",
+ fulfilled: /[A-Z]/.test(form.password),
+ },
+ {
+ description: "password_number_validation",
+ fulfilled: /\d/.test(form.password),
+ },
+ ]}
+ />
)}
@@ -167,23 +166,12 @@ const ResetPassword = (props: ResetPasswordProps) => {
name="confirm"
placeholder={t("confirm_password")}
onChange={handleChange}
- onFocus={() => setConfirmPasswordInputInFocus(true)}
- onBlur={() => setConfirmPasswordInputInFocus(false)}
/>
{errors.confirm && (
{errors.confirm}
)}
- {confirmPasswordInputInFocus &&
- form.confirm.length > 0 &&
- form.password.length > 0 &&
- validateRule(
- form.confirm === form.password,
- t("password_mismatch"),
- !form.password && form.password.length > 0,
- t("password_match"),
- )}
diff --git a/src/components/Facility/FacilityHome.tsx b/src/components/Facility/FacilityHome.tsx
index e6cddee5a70..5cee02880c0 100644
--- a/src/components/Facility/FacilityHome.tsx
+++ b/src/components/Facility/FacilityHome.tsx
@@ -1,4 +1,10 @@
import careConfig from "@careConfig";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@radix-ui/react-tooltip";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Hospital, MapPin, MoreVertical, Settings } from "lucide-react";
import { navigate } from "raviger";
@@ -219,32 +225,44 @@ export const FacilityHome = ({ facilityId }: Props) => {
)}
-
-
-
+
+
+
-
-
- {facilityData?.name}
-
+
+
+
+
+
+ {facilityData?.name}
+
+
+
+
+ {facilityData?.name}
+
+
+
+
-
+
-
+
{hasPermissionToEditCoverImage && (
{
-
+
diff --git a/src/components/Files/FilesTab.tsx b/src/components/Files/FilesTab.tsx
index 27779acec8c..a4a118d8792 100644
--- a/src/components/Files/FilesTab.tsx
+++ b/src/components/Files/FilesTab.tsx
@@ -595,7 +595,7 @@ const FileUploadDialog = ({
{fileUpload.files.length > 1 ? t("upload_files") : t("upload_file")}
-
+
{isPdf ? (
<>
{fileUpload.files.map((file, index) => (
diff --git a/src/components/Resource/ResourceBoard.tsx b/src/components/Resource/ResourceBoard.tsx
index 0ec92836de3..b048c3c6cd4 100644
--- a/src/components/Resource/ResourceBoard.tsx
+++ b/src/components/Resource/ResourceBoard.tsx
@@ -43,7 +43,7 @@ export default function BoardView() {
cacheBlacklist: ["title"],
});
const [boardFilter, setBoardFilter] = useState(ACTIVE);
- // eslint-disable-next-line
+
const appliedFilters = formatFilter(qParams);
const { t } = useTranslation();
diff --git a/src/components/Users/UserForm.tsx b/src/components/Users/UserForm.tsx
index 2211fbaf020..d5656c89694 100644
--- a/src/components/Users/UserForm.tsx
+++ b/src/components/Users/UserForm.tsx
@@ -29,7 +29,10 @@ import {
SelectValue,
} from "@/components/ui/select";
-import { validateRule } from "@/components/Users/UserFormValidations";
+import {
+ ValidationHelper,
+ validateRule,
+} from "@/components/Users/UserFormValidations";
import { GENDER_TYPES } from "@/common/constants";
import { GENDERS } from "@/common/constants";
@@ -68,22 +71,22 @@ export default function UserForm({
? z.string().optional()
: z
.string()
- .min(4, t("username_min_length_validation"))
- .max(16, t("username_max_length_validation"))
- .regex(/^[a-z0-9._-]*$/, t("username_characters_validation"))
- .regex(/^[a-z0-9].*[a-z0-9]$/, t("username_start_end_validation"))
+ .min(4, t("field_required"))
+ .max(16, t("username_not_valid"))
+ .regex(/^[a-z0-9._-]*$/, t("username_not_valid"))
+ .regex(/^[a-z0-9].*[a-z0-9]$/, t("username_not_valid"))
.refine(
(val) => !val.match(/(?:[._-]{2,})/),
- t("username_consecutive_validation"),
+ t("username_not_valid"),
),
password: isEditMode
? z.string().optional()
: z
.string()
- .min(8, t("password_length_validation"))
- .regex(/[a-z]/, t("password_lowercase_validation"))
- .regex(/[A-Z]/, t("password_uppercase_validation"))
- .regex(/[0-9]/, t("password_number_validation")),
+ .min(8, t("field_required"))
+ .regex(/[a-z]/, t("new_password_validation"))
+ .regex(/[A-Z]/, t("new_password_validation"))
+ .regex(/[0-9]/, t("new_password_validation")),
c_password: isEditMode ? z.string().optional() : z.string(),
first_name: z.string().min(1, t("field_required")),
last_name: z.string().min(1, t("field_required")),
@@ -120,6 +123,12 @@ export default function UserForm({
resolver: zodResolver(userFormSchema),
defaultValues: {
user_type: "staff",
+ username: "",
+ password: "",
+ c_password: "",
+ first_name: "",
+ last_name: "",
+ email: "",
phone_number: "",
alt_phone_number: "",
phone_number_is_whatsapp: true,
@@ -133,7 +142,6 @@ export default function UserForm({
}),
enabled: !!existingUsername,
});
-
useEffect(() => {
if (userData && isEditMode) {
const formData: Partial
= {
@@ -149,6 +157,9 @@ export default function UserForm({
}
}, [userData, form, isEditMode]);
+ const [isPasswordFieldFocused, setIsPasswordFieldFocused] = useState(false);
+ const [isUsernameFieldFocused, setIsUsernameFieldFocused] = useState(false);
+
//const userType = form.watch("user_type");
const usernameInput = form.watch("username");
const phoneNumber = form.watch("phone_number");
@@ -179,12 +190,7 @@ export default function UserForm({
const isInitialRender = usernameInput === "";
if (username?.message) {
- return validateRule(
- false,
- username.message,
- isInitialRender,
- t("username_valid"),
- );
+ return null;
} else if (isUsernameChecking) {
return (
@@ -363,10 +369,57 @@ export default function UserForm({
data-cy="username-input"
placeholder={t("username")}
{...field}
+ onFocus={() => setIsUsernameFieldFocused(true)}
+ onBlur={() => setIsUsernameFieldFocused(false)}
/>
- {renderUsernameFeedback(usernameInput ?? "")}
+ {isUsernameFieldFocused ? (
+ <>
+
+ = 4,
+ },
+ {
+ description: "username_max_length_validation",
+ fulfilled: (field.value || "").length <= 16,
+ },
+ {
+ description: "username_characters_validation",
+ fulfilled: /^[a-z0-9._-]*$/.test(
+ field.value || "",
+ ),
+ },
+ {
+ description: "username_start_end_validation",
+ fulfilled: /^[a-z0-9].*[a-z0-9]$/.test(
+ field.value || "",
+ ),
+ },
+ {
+ description: "username_consecutive_validation",
+ fulfilled: !/(?:[._-]{2,})/.test(
+ field.value || "",
+ ),
+ },
+ ]}
+ />
+
+
+ {renderUsernameFeedback(usernameInput || "")}
+
+ >
+ ) : (
+
+ )}
)}
/>
@@ -383,9 +436,41 @@ export default function UserForm({
data-cy="password-input"
placeholder={t("password")}
{...field}
+ onFocus={() => setIsPasswordFieldFocused(true)}
+ onBlur={() => setIsPasswordFieldFocused(false)}
/>
-
+ {isPasswordFieldFocused ? (
+
+ = 8,
+ },
+ {
+ description: "password_lowercase_validation",
+ fulfilled: /[a-z]/.test(field.value || ""),
+ },
+ {
+ description: "password_uppercase_validation",
+ fulfilled: /[A-Z]/.test(field.value || ""),
+ },
+ {
+ description: "password_number_validation",
+ fulfilled: /\d/.test(field.value || ""),
+ },
+ ]}
+ />
+
+ ) : (
+
+ )}
)}
/>
@@ -616,6 +701,7 @@ export default function UserForm({
type="submit"
className="w-full"
data-cy="submit-user-form"
+ variant="primary"
disabled={
isLoadingUser ||
!form.formState.isDirty ||
diff --git a/src/components/Users/UserFormValidations.tsx b/src/components/Users/UserFormValidations.tsx
index 4899a6f3d25..f07e08a9718 100644
--- a/src/components/Users/UserFormValidations.tsx
+++ b/src/components/Users/UserFormValidations.tsx
@@ -1,3 +1,5 @@
+import { Trans } from "react-i18next";
+
import CareIcon from "@/CAREUI/icons/CareIcon";
import { classNames } from "@/Utils/utils";
@@ -6,6 +8,54 @@ export type UserType = "doctor" | "nurse" | "staff" | "volunteer";
export type Gender = "male" | "female" | "non_binary" | "transgender";
+type Validation = {
+ description: string;
+ fulfilled: boolean;
+};
+
+type ValidationHelperProps = {
+ validations: Validation[];
+ successMessage: string;
+ isInputEmpty: boolean;
+};
+export const ValidationHelper = ({
+ validations,
+ successMessage,
+ isInputEmpty,
+}: ValidationHelperProps) => {
+ const unfulfilledValidations = validations.filter(
+ (validation) => !validation.fulfilled,
+ );
+
+ const allValid = unfulfilledValidations.length === 0 && !isInputEmpty;
+
+ return (
+
+ {isInputEmpty &&
+ validations.map((validation, index) => (
+
+
+
+ ))}
+ {!isInputEmpty &&
+ !allValid &&
+ unfulfilledValidations.map((validation, index) => (
+
+
+
+ ))}
+ {allValid && (
+ <>
+
+
+ {successMessage}
+
+ >
+ )}
+
+ );
+};
+
export const validateRule = (
isConditionMet: boolean,
initialMessage: JSX.Element | string,
diff --git a/src/components/Users/UserResetPassword.tsx b/src/components/Users/UserResetPassword.tsx
index 6b0d343b0dc..fed51b5d5b3 100644
--- a/src/components/Users/UserResetPassword.tsx
+++ b/src/components/Users/UserResetPassword.tsx
@@ -19,7 +19,7 @@ import {
} from "@/components/ui/form";
import { PasswordInput } from "@/components/ui/input-password";
-import { validateRule } from "@/components/Users/UserFormValidations";
+import { ValidationHelper } from "@/components/Users/UserFormValidations";
import { UpdatePasswordForm } from "@/components/Users/models";
import routes from "@/Utils/request/api";
@@ -152,36 +152,33 @@ export default function UserResetPassword({
className="text-small mt-2 pl-2 text-secondary-500"
aria-live="polite"
>
- {validateRule(
- field.value.length >= 8,
- t("password_length_validation"),
- !field.value,
- t("password_length_met"),
- )}
- {validateRule(
- /[a-z]/.test(field.value),
- t("password_lowercase_validation"),
- !field.value,
- t("password_lowercase_met"),
- )}
- {validateRule(
- /[A-Z]/.test(field.value),
- t("password_uppercase_validation"),
- !field.value,
- t("password_uppercase_met"),
- )}
- {validateRule(
- /\d/.test(field.value),
- t("password_number_validation"),
- !field.value,
- t("password_number_met"),
- )}
- {validateRule(
- field.value !== form.watch("old_password"),
- t("new_password_same_as_old"),
- !field.value,
- t("new_password_different_from_old"),
- )}
+ = 8,
+ },
+ {
+ description: "password_lowercase_validation",
+ fulfilled: /[a-z]/.test(field.value),
+ },
+ {
+ description: "password_uppercase_validation",
+ fulfilled: /[A-Z]/.test(field.value),
+ },
+ {
+ description: "password_number_validation",
+ fulfilled: /\d/.test(field.value),
+ },
+ {
+ description: "new_password_same_as_old",
+ fulfilled:
+ field.value !== form.watch("old_password"),
+ },
+ ]}
+ />
) : (
diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx
index 1a71cc04227..8a61c10c92a 100644
--- a/src/components/ui/sidebar.tsx
+++ b/src/components/ui/sidebar.tsx
@@ -1,4 +1,5 @@
import { Slot } from "@radix-ui/react-slot";
+import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
import { VariantProps, cva } from "class-variance-authority";
import { PanelLeftClose, PanelRightClose } from "lucide-react";
import * as React from "react";
@@ -9,7 +10,7 @@ import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
-import { Sheet, SheetContent } from "@/components/ui/sheet";
+import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet";
import { Skeleton } from "@/components/ui/skeleton";
import {
Tooltip,
@@ -207,6 +208,9 @@ const Sidebar = React.forwardRef<
}
side={side}
>
+
+ Sidebar
+
{children}