first commit

This commit is contained in:
2026-03-26 08:11:29 +03:30
parent f9e3d66a19
commit b8f6526ba4
225 changed files with 18865 additions and 151 deletions

22
components.json Normal file
View File

@@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}

1
debug.log Normal file
View File

@@ -0,0 +1 @@
[0212/070717.490:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: The system cannot find the file specified. (0x2)

View File

@@ -1,6 +1,16 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'http',
hostname: 'localhost',
port: '4000',
pathname: '/uploads/**',
},
],
},
/* config options here */ /* config options here */
}; };

2740
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,19 +9,48 @@
"lint": "eslint" "lint": "eslint"
}, },
"dependencies": { "dependencies": {
"@emotion/styled": "^11.14.1",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@tanstack/react-query": "^5.90.12",
"axios": "^1.13.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"formik": "^2.4.9",
"jsonwebtoken": "^9.0.3",
"jwt-decode": "^4.0.0",
"lucide-react": "^0.561.0",
"next": "15.5.7",
"next-nprogress-bar": "^2.4.7",
"quill": "^2.0.3",
"react": "19.1.0", "react": "19.1.0",
"react-day-picker": "^9.13.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"next": "15.5.7" "react-dropzone": "^14.3.8",
"react-toastify": "^11.0.5",
"tailwind-merge": "^3.4.0",
"yup": "^1.7.1"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5", "@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.5.7", "eslint-config-next": "15.5.7",
"@eslint/eslintrc": "^3" "tailwindcss": "^4",
"tw-animate-css": "^1.4.0",
"typescript": "^5"
} }
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

13
src/app/(auth)/layout.tsx Normal file
View File

@@ -0,0 +1,13 @@
"use client";
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}

View File

@@ -0,0 +1,24 @@
"use client";
import LoginForm from "@/components/forms/login/LoginForm";
import {useUserLogin} from "@/hooks";
import {useRouter} from "next/navigation";
import React from "react";
export default function Page() {
const router = useRouter();
const {mutateAsync, isPending} = useUserLogin();
return (
<div className="flex flex-col items-center justify-center h-screen">
<div className="rounded-lg overflow-hidden min-w-[400px]">
<div className="bg-neutral-200 text-black font-medium p-4 text-center">
فرم ورود
</div>
<LoginForm
router={router}
loginFn={mutateAsync}
loginPending={isPending}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import React from 'react'
export default function Page() {
return (
<div>
</div>
)
}

View File

@@ -0,0 +1,18 @@
"use client"
import ResetPasswordForm from '@/components/forms/reset-password/ResetPasswordForm'
import { useRouter } from 'next/navigation'
import React from 'react'
export default function Page() {
const router = useRouter();
return (
<div className="flex flex-col items-center justify-center h-screen">
<div className="rounded-lg overflow-hidden min-w-[400px]">
<div className="bg-neutral-200 text-black font-medium p-4 text-center">
فرم فراموشی رمز عبور
</div>
<ResetPasswordForm router={router} />
</div>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Loading() {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin w-8 h-8 border-4 border-neutral-800 border-t-transparent rounded-full"></div>
</div>
);
}

View File

@@ -0,0 +1,26 @@
"use client";
import UpdateConfigsForm from "@/components/forms/configs/ConfigsForm";
import Loader from "@/components/Loader";
import {useGetAllConfigs, useUpdateConfigs} from "@/hooks/configs";
import {useRouter} from "next/navigation";
import React, {Suspense} from "react";
export default function Page() {
const router = useRouter();
const {data} = useGetAllConfigs();
const {mutateAsync, isPending} = useUpdateConfigs();
return (
<div>
<Suspense fallback={<Loader />}>
{data && data?.data?.length && (
<UpdateConfigsForm
router={router}
initialData={data?.data}
onSubmit={mutateAsync}
updatePending={isPending}
/>
)}
</Suspense>
</div>
);
}

View File

@@ -0,0 +1,7 @@
export default function Loading() {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin w-8 h-8 border-4 border-neutral-800 border-t-transparent rounded-full"></div>
</div>
)
}

View File

@@ -0,0 +1,25 @@
import DashboardCDNServerStatus from "@/components/DashboardCDNServerStatus";
import DashboardStatistics from "@/components/DashboardStatistics";
import Loader from "@/components/Loader";
import RoleGuard from "@/components/RoleGuard";
import React, {Suspense} from "react";
export default function Page() {
return (
<div className="grid grid-cols-12 gap-x-4">
<div className="col-span-8">
<Suspense fallback={<Loader />}>
<DashboardStatistics />
</Suspense>
</div>
<div className="col-span-4">
<Suspense fallback={<Loader />}>
<RoleGuard roles={["ADMIN","DEVELOPER"]}>
<DashboardCDNServerStatus />
</RoleGuard>
</Suspense>
</div>
</div>
);
}

View File

@@ -0,0 +1,7 @@
export default function Loading() {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin w-8 h-8 border-4 border-neutral-800 border-t-transparent rounded-full"></div>
</div>
);
}

View File

@@ -0,0 +1,33 @@
"use client";
import UpdateDefaultForm from "@/components/forms/default/UpdateDefaultsForm";
import Loader from "@/components/Loader";
import {useGetAllDefaults, useUpdateDefaults} from "@/hooks/defaults";
import { useGetLanguages } from "@/hooks/languages";
import {useRouter} from "next/navigation";
import React, {Suspense} from "react";
export default function Page() {
const router = useRouter();
const {data, isLoading} = useGetAllDefaults();
const {mutateAsync, isPending} = useUpdateDefaults();
const {
data: languages,
} = useGetLanguages();
return (
<div>
<Suspense fallback={<Loader />}>
{data && languages && (
<UpdateDefaultForm
router={router}
languages={languages?.data}
updateFn={mutateAsync}
updatePending={isPending}
preValues={data?.data}
preValuesLoading={isLoading}
/>
)}
</Suspense>
</div>
);
}

View File

@@ -0,0 +1,7 @@
export default function Loading() {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin w-8 h-8 border-4 border-neutral-800 border-t-transparent rounded-full"></div>
</div>
)
}

View File

@@ -0,0 +1,64 @@
"use client";
import UpdateDoctorForm from "@/components/forms/doctor/update/UpdateDoctorForm";
import Loader from "@/components/Loader";
import SectionTitle from "@/components/SectionTitle";
import { useGetAllExpertiseList } from "@/hooks/expertise";
import { useGetLanguages } from "@/hooks/languages";
import {useGetSingleUser, useUpdateUser} from "@/hooks/users";
import Link from "next/link";
import {useParams, useRouter} from "next/navigation";
import {Suspense, useState} from "react";
export default function Page() {
const params = useParams();
const id = params?.id;
const router = useRouter();
const [loading] = useState(false);
const {
data: languages,
} = useGetLanguages();
const {
data: expertises,
} = useGetAllExpertiseList();
const {
data: singleUser,
} = useGetSingleUser(id?.toString() ?? "");
const {mutateAsync} = useUpdateUser();
const {data} = singleUser || {};
return (
<div>
<div className="flex items-center justify-between mb-10">
<SectionTitle label="فرم ویرایش عضو دپارتمان " />
<div className="flex items-center justify-center gap-x-4">
<Link
href={`/department/members/new`}
className="text-sm bg-[#313131] px-4 py-2 rounded-lg text-white"
>
افزودن عضو جدید
</Link>
<Link
href={`/department/members`}
className="text-sm bg-primary px-4 py-2 rounded-lg text-white"
>
مشاهده لیست اعضاء دپارتمان
</Link>
</div>
</div>
<Suspense fallback={<Loader />}>
{data && expertises && languages && !loading && (
<UpdateDoctorForm
expertises={expertises?.data}
router={router}
languages={languages?.data}
preValues={data}
id={id?.toString() ??""}
updateFn={mutateAsync}
/>
)}
</Suspense>
</div>
);
}

View File

@@ -0,0 +1,7 @@
"use client"
import { notFound } from 'next/navigation'
export default function Page() {
return notFound
}

View File

@@ -0,0 +1,103 @@
"use client";
import DenyAccess from "@/components/DenyAccess";
import CreateDepartmentMember from "@/components/forms/department/new/CreateDepartmentMember";
import Loader from "@/components/Loader";
import SectionTitle from "@/components/SectionTitle";
import {API_URL} from "@/constants";
import {Expertise, Language} from "@/types";
import Link from "next/link";
import {useRouter} from "next/navigation";
import {Suspense, useEffect, useState} from "react";
async function fetchLanguages() {
const res = await fetch(`${API_URL}/language/get/all`,{cache:"no-cache"});
if (!res.ok && res.status==500) {
throw new Error("Failed to get data");
}
if(!res.ok && res.status==404){
return []
}
const data = await res.json();
return data;
}
async function fetchExpertise() {
const res = await fetch(`${API_URL}/expertise/fa/get/all/list`,{cache:"no-cache"});
if (!res.ok && res.status==500) {
throw new Error("Failed to get data");
}
if(!res.ok && res.status==404){
return []
}
const data = await res.json();
return data;
}
export default function Page() {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [data, setData] = useState<Language[]>([]);
const [expertises, setExpertises] = useState<Expertise[]>([]);
useEffect(() => {
let active = true;
setLoading(true);
fetchExpertise()
.then((res) => {
if (!active) return;
setExpertises(res.data);
})
.catch(console.error)
.finally(() => active && setLoading(false));
return () => {
active = false;
};
}, []);
useEffect(() => {
let active = true;
setLoading(true);
fetchLanguages()
.then((res) => {
if (!active) return;
setData(res.data);
})
.catch(console.error)
.finally(() => active && setLoading(false));
return () => {
active = false;
};
}, []);
if (loading) {
return <Loader />;
}
return (
<div>
<div className="flex items-center justify-between mb-10">
<SectionTitle label="فرم افزودن عضو جدید" />
<Link href={`/department/members`} className="text-sm bg-primary px-4 py-2 rounded-lg text-white">مشاهده لیست اعضاء دپارتمان</Link>
</div>
<Suspense fallback={<Loader />}>
{data?.length > 0 ? expertises?.length > 0 ? <CreateDepartmentMember
expertises={expertises}
router={router}
languages={data}
/> : <DenyAccess label="تخصص" link="/expertise/new"/> : <DenyAccess label="زبان" link="/languages"/> }
</Suspense>
</div>
);
}

View File

@@ -0,0 +1,49 @@
import DepartmentMembersTable from "@/components/DepartmentMembersTable";
import Loader from "@/components/Loader";
import RoleGuard from "@/components/RoleGuard";
import SearchBox from "@/components/SearchBox";
import UsersTableExport from "@/components/usersTableExport";
import {Plus} from "lucide-react";
import Link from "next/link";
import React, {Suspense} from "react";
export default function Page() {
return (
<>
<section>
<div className="flex items-center justify-between">
<div className="flex items-center justify-start gap-x-8">
<SearchBox
inputName="search"
label="نام خانوادگی"
hasLabel
placeholder="جستجو نام خانوادگی اعضا ..."
route="department"
/>
</div>
<div className="flex items-center justify-end gap-x-7">
<RoleGuard roles={["ADMIN", "DEVELOPER"]}>
<Link
href={"/department/members/new"}
className="text-white bg-primary py-1.5 px-3 rounded-lg flex items-center gap-x-2 text-sm"
>
<Plus size={"20"} />
<span>افزودن عضو جدید</span>
</Link>
</RoleGuard>
<RoleGuard roles={["ADMIN","DEVELOPER"]}>
<UsersTableExport table="DEPARTMENT" />
</RoleGuard>
</div>
</div>
<div className="space-y-4 mt-10">
<Suspense fallback={<Loader />}>
<DepartmentMembersTable />
</Suspense>
</div>
</section>
</>
);
}

View File

@@ -0,0 +1,64 @@
"use client";
import UpdateDoctorForm from "@/components/forms/doctor/update/UpdateDoctorForm";
import Loader from "@/components/Loader";
import SectionTitle from "@/components/SectionTitle";
import { useGetAllExpertiseList } from "@/hooks/expertise";
import { useGetLanguages } from "@/hooks/languages";
import {useGetSingleUser, useUpdateUser} from "@/hooks/users";
import Link from "next/link";
import {useParams, useRouter} from "next/navigation";
import {Suspense, useState} from "react";
export default function Page() {
const params = useParams();
const id = params?.id;
const router = useRouter();
const [loading] = useState(false);
const {
data: languages,
} = useGetLanguages();
const {
data: expertises,
} = useGetAllExpertiseList();
const {
data: singleUser,
} = useGetSingleUser(id?.toString() ?? "");
const {mutateAsync} = useUpdateUser();
const {data} = singleUser || {};
return (
<div>
<div className="flex items-center justify-between mb-10">
<SectionTitle label="فرم ویرایش پزشک " />
<div className="flex items-center justify-center gap-x-4">
<Link
href={`/doctors/new`}
className="text-sm bg-[#313131] px-4 py-2 rounded-lg text-white"
>
افزودن پزشک جدید
</Link>
<Link
href={`/doctors`}
className="text-sm bg-primary px-4 py-2 rounded-lg text-white"
>
مشاهده لیست پزشکان
</Link>
</div>
</div>
<Suspense fallback={<Loader />}>
{data && expertises && languages && !loading && (
<UpdateDoctorForm
expertises={expertises?.data}
router={router}
languages={languages?.data}
preValues={data}
id={id?.toString() ??""}
updateFn={mutateAsync}
/>
)}
</Suspense>
</div>
);
}

View File

@@ -0,0 +1,10 @@
"use client"
import { notFound } from "next/navigation";
export default function Page() {
return notFound();
}

View File

@@ -0,0 +1,7 @@
export default function Loading() {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin w-8 h-8 border-4 border-neutral-800 border-t-transparent rounded-full"></div>
</div>
)
}

View File

@@ -0,0 +1,103 @@
"use client";
import DenyAccess from "@/components/DenyAccess";
import CreateDoctorForm from "@/components/forms/doctor/create/CreateDoctorForm";
import Loader from "@/components/Loader";
import SectionTitle from "@/components/SectionTitle";
import {API_URL} from "@/constants";
import {Expertise, Language} from "@/types";
import Link from "next/link";
import {useRouter} from "next/navigation";
import {Suspense, useEffect, useState} from "react";
async function fetchLanguages() {
const res = await fetch(`${API_URL}/language/get/all`,{cache:"no-cache"});
if (!res.ok && res.status==500) {
throw new Error("Failed to get data");
}
if(!res.ok && res.status==404){
return []
}
const data = await res.json();
return data;
}
async function fetchExpertise() {
const res = await fetch(`${API_URL}/expertise/fa/get/all/list`,{cache:"no-cache"});
if (!res.ok && res.status==500) {
throw new Error("Failed to get data");
}
if(!res.ok && res.status==404){
return []
}
const data = await res.json();
return data;
}
export default function Page() {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [data, setData] = useState<Language[]>([]);
const [expertises, setExpertises] = useState<Expertise[]>([]);
useEffect(() => {
let active = true;
setLoading(true);
fetchExpertise()
.then((res) => {
if (!active) return;
setExpertises(res.data);
})
.catch(console.error)
.finally(() => active && setLoading(false));
return () => {
active = false;
};
}, []);
useEffect(() => {
let active = true;
setLoading(true);
fetchLanguages()
.then((res) => {
if (!active) return;
setData(res.data);
})
.catch(console.error)
.finally(() => active && setLoading(false));
return () => {
active = false;
};
}, []);
if (loading) {
return <Loader />;
}
return (
<div>
<div className="flex items-center justify-between mb-10">
<SectionTitle label="فرم افزودن پزشک جدید" />
<Link href={`/doctors`} className="text-sm bg-primary px-4 py-2 rounded-lg text-white">مشاهده لیست پزشکان</Link>
</div>
<Suspense fallback={<Loader />}>
{data?.length > 0 ? expertises?.length > 0 ? <CreateDoctorForm
expertises={expertises}
router={router}
languages={data}
/> : <DenyAccess label="تخصص" link="/expertise/new"/> : <DenyAccess label="زبان" link="/languages"/> }
</Suspense>
</div>
);
}

View File

@@ -0,0 +1,51 @@
import DoctorsTable from "@/components/DoctorsTable";
import ExpertiseFilterBox from "@/components/ExpertiseFilterBox";
import Loader from "@/components/Loader";
import RoleGuard from "@/components/RoleGuard";
import SearchBox from "@/components/SearchBox";
import UsersTableExport from "@/components/usersTableExport";
import {Plus} from "lucide-react";
import Link from "next/link";
import React, {Suspense} from "react";
export default function Page() {
return (
<>
<section>
<div className="flex items-center justify-between">
<div className="flex items-center justify-start gap-x-8">
<SearchBox
inputName="search"
label="جستجو نام خانوادگی"
hasLabel
placeholder="جستجو نام خانوادگی پزشک ..."
route="doctors"
/>
<ExpertiseFilterBox />
</div>
<div className="flex items-center justify-end gap-x-7">
<RoleGuard roles={["ADMIN", "DEVELOPER", "COORDINATOR"]}>
<Link
href={"/doctors/new"}
className="text-white bg-primary py-1.5 px-3 rounded-lg flex items-center gap-x-2 text-sm"
>
<Plus size={"20"} />
<span>افزودن پزشک جدید</span>
</Link>
</RoleGuard>
<RoleGuard roles={["ADMIN","DEVELOPER","COORDINATOR"]}>
<UsersTableExport table="DOCTOR" />
</RoleGuard>
</div>
</div>
<div className="space-y-4 mt-10">
<Suspense fallback={<Loader />}>
<DoctorsTable />
</Suspense>
</div>
</section>
</>
);
}

View File

@@ -0,0 +1,62 @@
"use client";
import UpdateExpertiseForm from "@/components/forms/expertise/edit/UpdateExpertiseForm";
import Loader from "@/components/Loader";
import SectionTitle from "@/components/SectionTitle";
import {useGetSingleExpertise, useUpdateExpertise} from "@/hooks/expertise";
import {useGetLanguages} from "@/hooks/languages";
import Link from "next/link";
import {useParams, useRouter} from "next/navigation";
import {Suspense, useState} from "react";
export default function Page() {
const params = useParams();
const id = params?.id;
const router = useRouter();
const [loading] = useState(false);
const {
data: languages,
} = useGetLanguages();
const {
data: singleExpertise,
} = useGetSingleExpertise(id?.toString() ?? "");
const {isPending, mutateAsync} = useUpdateExpertise();
const {data} = singleExpertise || {};
return (
<div>
<div className="flex items-center justify-between mb-10">
<SectionTitle label="فرم ویرایش تخصص " />
<div className="flex items-center justify-center gap-x-4">
<Link
href={`/expertise/new`}
className="text-sm bg-[#313131] px-4 py-2 rounded-lg text-white"
>
افزودن تخصص جدید
</Link>
<Link
href={`/expertise`}
className="text-sm bg-primary px-4 py-2 rounded-lg text-white"
>
مشاهده لیست تخصص ها
</Link>
</div>
</div>
<Suspense fallback={<Loader />}>
{data && languages && !loading && (
<UpdateExpertiseForm
preValues={singleExpertise?.data}
router={router}
languages={languages?.data}
updatePending={isPending}
id={id?.toString() ?? ""}
updateFn={mutateAsync}
/>
)}
</Suspense>
</div>
);
}

View File

@@ -0,0 +1,7 @@
export default function Loading() {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin w-8 h-8 border-4 border-neutral-800 border-t-transparent rounded-full"></div>
</div>
)
}

View File

@@ -0,0 +1,9 @@
import React from 'react'
export default function Page() {
return (
<div>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Loading() {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin w-8 h-8 border-4 border-neutral-800 border-t-transparent rounded-full"></div>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Loading() {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin w-8 h-8 border-4 border-neutral-800 border-t-transparent rounded-full"></div>
</div>
)
}

View File

@@ -0,0 +1,40 @@
"use client";
import DenyAccess from "@/components/DenyAccess";
import CreateExpertiseForm from "@/components/forms/expertise/new/CreateExpertiseForm";
import Loader from "@/components/Loader";
import SectionTitle from "@/components/SectionTitle";
import {useCreateExpertise} from "@/hooks/expertise";
import {useGetLanguages} from "@/hooks/languages";
import Link from "next/link";
import {useRouter} from "next/navigation";
import React, {Suspense} from "react";
export default function Page() {
const router = useRouter();
const {data} = useGetLanguages();
const {mutateAsync, isPending} = useCreateExpertise();
return (
<div>
<div className="flex items-center justify-between mb-10">
<SectionTitle label="فرم افزودن تخصص جدید" />
<Link
href={`/expertise`}
className="text-sm bg-primary px-4 py-2 rounded-lg text-white"
>
مشاهده لیست تخصص ها
</Link>
</div>
<Suspense fallback={<Loader size="4" />}>
{
data?.data ? data?.data?.length > 0 ? <CreateExpertiseForm
router={router}
createFn={mutateAsync}
createPending={isPending}
languages={data?.data}
/> : <DenyAccess label="زبان" link="/languages"/> : <div>خطا در دریافت اطلاعات زبان ها</div>
}
</Suspense>
</div>
);
}

View File

@@ -0,0 +1,52 @@
"use client";
import ExpertiseTable from "@/components/ExpertiseTable";
import LanguagesFilterBox from "@/components/LanguagesFilterBox";
import RoleGuard from "@/components/RoleGuard";
import SearchBox from "@/components/SearchBox";
import { Spinner } from "@/components/ui/spinner";
import {Plus} from "lucide-react";
import Link from "next/link";
import React, { Suspense, useState } from "react";
export default function Page() {
const [lang,setLang]=useState<string>("fa");
return (
<>
<section>
<div className="flex items-center justify-between">
<div className="flex items-center justify-start gap-x-8">
<SearchBox
inputName="search"
label="عنوان"
hasLabel
route="expertise"
/>
<LanguagesFilterBox lang={lang} setLang={setLang}/>
</div>
<div className="flex items-center justify-end gap-x-7">
<RoleGuard roles={["DEVELOPER"]}>
<Link
href={"/expertise/new"}
className="text-white bg-primary py-1.5 px-3 rounded-lg flex items-center gap-x-2 text-sm"
>
<Plus size={"20"} />
<span>افزودن تخصص جدید</span>
</Link>
</RoleGuard>
{/* <RoleGuard roles={["ADMIN", "DEVELOPER", "COORDINATOR"]}>
<UsersTableExport table="DOCTOR" />
</RoleGuard> */}
</div>
</div>
<div className="space-y-4 mt-10">
<Suspense fallback={<Spinner />}>
<ExpertiseTable lang={lang}/>
</Suspense>
</div>
</section>
</>
);
}

View File

@@ -0,0 +1,7 @@
export default function Loading() {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin w-8 h-8 border-4 border-neutral-800 border-t-transparent rounded-full"></div>
</div>
)
}

View File

@@ -0,0 +1,52 @@
"use client";
import CreateLanguageForm from "@/components/forms/languages/new/CreateLanguageForm";
import LanguagesTable from "@/components/LangaugesTable";
import Loader from "@/components/Loader";
import RoleGuard from "@/components/RoleGuard";
import SearchBox from "@/components/SearchBox";
import {Dialog} from "@/components/ui/dialog";
import {useCreateLanguage} from "@/hooks/languages";
import { useQueryClient } from "@tanstack/react-query";
import {useRouter} from "next/navigation";
import React, {Suspense} from "react";
export default function Page() {
const router = useRouter();
const queryClient = useQueryClient();
const {mutateAsync, isPending} = useCreateLanguage();
return (
<>
<section>
<div className="flex items-center justify-between">
<div className="flex items-center justify-start gap-x-8">
<SearchBox
inputName="search"
label="عنوان"
hasLabel
placeholder="جستجو عنوان زبان ..."
route="languages"
/>
</div>
<div className="flex items-center justify-end gap-x-7">
<RoleGuard roles={["ADMIN", "DEVELOPER"]}>
<Dialog>
<CreateLanguageForm
router={router}
createFn={mutateAsync}
createPending={isPending}
queryClient={queryClient}
/>
</Dialog>
</RoleGuard>
</div>
</div>
<div className="space-y-4 mt-10">
<Suspense fallback={<Loader />}>
<LanguagesTable />
</Suspense>
</div>
</section>
</>
);
}

View File

@@ -0,0 +1,53 @@
import type {Metadata} from "next";
import "../globals.css";
import ProfileDropdown from "@/components/ProfileDropdown";
import {Button} from "@/components/ui/button";
import {Bell} from "lucide-react";
import SideMenu from "@/components/SideMenu";
import PanelQueryProvider from "@/components/PanelQueryProvider";
import AuthGuard from "@/components/AuthGuard";
import { Suspense } from "react";
import Loader from "@/components/Loader";
import { Badge } from "@/components/ui/badge";
export const metadata: Metadata = {
title: "پنل مدیریت سایت ipd",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<div className="flex items-start w-full text-[#313131]">
<PanelQueryProvider>
<AuthGuard>
<SideMenu />
<section className="w-full">
<div className="h-[60px] z-30 shadow-sm w-full pr-[var(--panel-width-padding)] flex items-center justify-between gap-x-5 bg-white pl-10">
<div className="flex items-center justify-start gap-x-4">
<Suspense fallback={<Loader />}>
<ProfileDropdown />
</Suspense>
<Button variant="ghost">
<Bell />
</Button>
<Badge variant={"secondary"} className="font-light text-base">
{new Date().toLocaleDateString("fa-IR")}
</Badge>
</div>
<h4 className="font-medium">
پنل مدیریت بیماران بین الملل بیمارستان شمال
</h4>
</div>
<div className="pl-10 py-10 pr-[var(--panel-width-padding))]">
{children}
</div>
</section>
</AuthGuard>
</PanelQueryProvider>
</div>
);
}

View File

@@ -0,0 +1,7 @@
export default function Loading() {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin w-8 h-8 border-4 border-neutral-800 border-t-transparent rounded-full"></div>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Loading() {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin w-8 h-8 border-4 border-neutral-800 border-t-transparent rounded-full"></div>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Loading() {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin w-8 h-8 border-4 border-neutral-800 border-t-transparent rounded-full"></div>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Loading() {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin w-8 h-8 border-4 border-neutral-800 border-t-transparent rounded-full"></div>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Loading() {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin w-8 h-8 border-4 border-neutral-800 border-t-transparent rounded-full"></div>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Loading() {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin w-8 h-8 border-4 border-neutral-800 border-t-transparent rounded-full"></div>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Loading() {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin w-8 h-8 border-4 border-neutral-800 border-t-transparent rounded-full"></div>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Loading() {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin w-8 h-8 border-4 border-neutral-800 border-t-transparent rounded-full"></div>
</div>
)
}

View File

@@ -0,0 +1,43 @@
import React from "react";
async function fetchData() {
const res = await fetch("http://localhost:4000/logs", { cache: "no-cache" });
if (!res.ok && res.status == 500) {
throw new Error("Failed to fetch data");
}
if (!res.ok && res.status == 404) {
return "";
}
const data = await res.json();
return data;
}
interface LogsType {
level: string;
message: { method: string; timestamp: Date; url: string };
}
export default async function Page() {
const data = await fetchData();
return (
<div>
<div className="space-y-5">
{data.logs
.map((item: string) => JSON.parse(item))
.map((log: LogsType, index: number) => (
<div key={index} className="bg-white p-4 rounded-4xl space-y-4">
<div>Level : {log.level}</div>
<div>message method : {log.message.method}</div>
<div>
message timestamp :{" "}
{new Date(log.message.timestamp).toLocaleString("fa-IR")}
</div>
<div>url : {log.message.url}</div>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,73 @@
"use client";
import UpdateMedicalPackageForm from "@/components/forms/medical-packages/edit/UpdateMedicalPackageForm";
import Loader from "@/components/Loader";
import SectionTitle from "@/components/SectionTitle";
import {useGetLanguages} from "@/hooks/languages";
import {
useGetAllMedicalPackagesParent,
useGetSingleMedicalPackage,
useUpdateMedicalPackage,
} from "@/hooks/medical-package";
import {useQueryClient} from "@tanstack/react-query";
import Link from "next/link";
import {useParams, useRouter} from "next/navigation";
import {Suspense, useState} from "react";
export default function Page() {
const params = useParams();
const id = params?.id;
const router = useRouter();
const queryClient = useQueryClient();
const [lang] = useState("fa");
const [loading] = useState(false);
const {
data: languages,
} = useGetLanguages();
const {
data: singleMedicalPackage,
} = useGetSingleMedicalPackage({id: id?.toString() ?? "", lang});
const {data: parents} =
useGetAllMedicalPackagesParent("fa");
const {isPending, mutateAsync} = useUpdateMedicalPackage();
const {data} = singleMedicalPackage || {};
return (
<div>
<div className="flex items-center justify-between mb-10">
<div className="flex items-center gap-x-4">
<SectionTitle label="فرم ویرایش خدمت پزشکی " />
</div>
<div className="flex items-center justify-center gap-x-4">
<Link
href={`/medical-packages/new`}
className="text-sm bg-[#313131] px-4 py-2 rounded-lg text-white"
>
افزودن خدمت پزشکی جدید
</Link>
<Link
href={`/medical-packages`}
className="text-sm bg-primary px-4 py-2 rounded-lg text-white"
>
مشاهده لیست خدمات پزشکی
</Link>
</div>
</div>
<Suspense fallback={<Loader />}>
{data && languages && !loading && (
<UpdateMedicalPackageForm
preValues={singleMedicalPackage?.data}
router={router}
languages={languages?.data}
updatePending={isPending}
id={id?.toString() ?? ""}
updateFn={mutateAsync}
parents={parents?.data}
queryClient={queryClient}
/>
)}
</Suspense>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import React from 'react'
export default function Page() {
return (
<div>
</div>
)
}

View File

@@ -0,0 +1,30 @@
"use client";
import DenyAccess from "@/components/DenyAccess";
import CreateMedicalPackageForm from "@/components/forms/medical-packages/new/CreateMedicalPackageForm";
import Loader from "@/components/Loader";
import {useGetLanguages} from "@/hooks/languages";
import {useCreateMedicalPackage, useGetAllMedicalPackagesParent} from "@/hooks/medical-package";
import {useQueryClient} from "@tanstack/react-query";
import React, {Suspense} from "react";
export default function Page() {
const {data} = useGetLanguages();
const {mutateAsync, isPending} = useCreateMedicalPackage();
const {data:parents}=useGetAllMedicalPackagesParent("fa")
const queryClient = useQueryClient();
return (
<div>
<Suspense fallback={<Loader />}>
{data?.data?.length > 0 ? (
<CreateMedicalPackageForm
languages={data?.data}
createFn={mutateAsync}
createPending={isPending}
queryClient={queryClient}
parents={parents?.data}
/>
) : <DenyAccess label="زبان" link="/languages"/> }
</Suspense>
</div>
);
}

View File

@@ -0,0 +1,49 @@
"use client";
import LanguagesFilterBox from "@/components/LanguagesFilterBox";
import Loader from "@/components/Loader";
import MedicalPackagesTable from "@/components/MedicalPackagesTable";
import RoleGuard from "@/components/RoleGuard";
import SearchBox from "@/components/SearchBox";
import {Plus} from "lucide-react";
import Link from "next/link";
import React, {Suspense, useState} from "react";
export default function Page() {
const [lang, setLang] = useState<string>("fa");
return (
<section>
<div className="flex items-center justify-between">
<div className="flex items-center justify-start gap-x-8">
<SearchBox
inputName="search"
label=" نام پکیج"
hasLabel
route="medical-packages"
/>
<LanguagesFilterBox setLang={setLang} lang={lang} />
</div>
<div className="flex items-center justify-end gap-x-7">
<RoleGuard roles={["ADMIN", "DEVELOPER", "COORDINATOR"]}>
<Link
href={"/medical-packages/new"}
className="text-white bg-primary py-1.5 px-3 rounded-lg flex items-center gap-x-2 text-sm"
>
<Plus size={"20"} />
<span>افزودن پکیج جدید</span>
</Link>
</RoleGuard>
{/* <RoleGuard roles={["ADMIN", "DEVELOPER", "COORDINATOR"]}>
<UsersTableExport table="DOCTOR" />
</RoleGuard> */}
</div>
</div>
<div className="space-y-4 mt-10">
<Suspense fallback={<Loader />}>
<MedicalPackagesTable lang={lang} />
</Suspense>
</div>
</section>
);
}

View File

@@ -0,0 +1,15 @@
"use client";
import CreateOnlineCaseForm from "@/components/forms/online-case/new/CreateOnlineCaseForm";
import { useGetAllCountries } from "@/hooks/country";
import {useRouter} from "next/navigation";
import React from "react";
export default function Page() {
const router = useRouter();
const {data:countries,isLoading:fetchCountriesLoading}=useGetAllCountries()
return (
<div>
<CreateOnlineCaseForm router={router} countries={countries?.data} fetchCountriesLoading={fetchCountriesLoading} />
</div>
);
}

View File

@@ -0,0 +1,46 @@
import Loader from "@/components/Loader";
import OnlineCasesTable from "@/components/OnlineCasesTable";
import RoleGuard from "@/components/RoleGuard";
import SearchBox from "@/components/SearchBox";
import {Plus} from "lucide-react";
import Link from "next/link";
import React, {Suspense} from "react";
export default function Page() {
return (
<>
<section>
<div className="flex items-center justify-between">
<div className="flex items-center justify-start gap-x-8">
<SearchBox
inputName="search"
label="جستجو نام خانوادگی"
hasLabel
placeholder="جستجو نام خانوادگی پزشک ..."
route="doctors"
/>
</div>
<div className="flex items-center justify-end gap-x-7">
<RoleGuard roles={["ADMIN", "DEVELOPER", "COORDINATOR"]}>
<Link
href={"/online-case/new"}
className="text-white bg-primary py-1.5 px-3 rounded-lg flex items-center gap-x-2 text-sm"
>
<Plus size={"20"} />
<span>افزودن کیس جدید</span>
</Link>
</RoleGuard>
{/* <RoleGuard roles={["ADMIN","DEVELOPER","COORDINATOR"]}>
<UsersTableExport table="DOCTOR" />
</RoleGuard> */}
</div>
</div>
<div className="space-y-4 mt-10">
<Suspense fallback={<Loader />}>
<OnlineCasesTable />
</Suspense>
</div>
</section>
</>
);
}

View File

@@ -0,0 +1,66 @@
"use client";
import UpdatePatientForm from "@/components/forms/patients/edit/UpdatePatientForm";
import Loader from "@/components/Loader";
import SectionTitle from "@/components/SectionTitle";
import {useGetAllCountries} from "@/hooks/country";
import {useGetSinglePatient, useUpdatePatient} from "@/hooks/patients";
import {useUpdateUser} from "@/hooks/users";
import Link from "next/link";
import {useParams, useRouter} from "next/navigation";
import {Suspense, useState} from "react";
export default function Page() {
const router = useRouter();
const {data: countries, isLoading: fetchCountriesLoading} =
useGetAllCountries();
const params = useParams();
const id = params?.id;
const [loading, setLoading] = useState(false);
const {
data: singleUser,
isLoading,
error,
} = useGetSinglePatient(id?.toString() ?? "");
const {isPending, mutateAsync} = useUpdatePatient();
const {data} = singleUser || {};
return (
<div>
<div className="flex items-center justify-between mb-10">
<SectionTitle label="فرم ویرایش اطلاعات بیمار " />
<div className="flex items-center justify-center gap-x-4">
<Link
href={`/patients/new`}
className="text-sm bg-[#313131] px-4 py-2 rounded-lg text-white"
>
افزودن بیمار جدید
</Link>
<Link
href={`/patients`}
className="text-sm bg-primary px-4 py-2 rounded-lg text-white"
>
مشاهده لیست بیماران
</Link>
</div>
</div>
<Suspense fallback={<Loader />}>
{data && countries && (
<UpdatePatientForm
router={router}
id={id?.toString() || ""}
preValues={data}
updateFn={mutateAsync}
countries={countries?.data}
fetchCountriesLoading={fetchCountriesLoading}
/>
)}
</Suspense>
</div>
);
}

View File

@@ -0,0 +1,7 @@
export default function Loading() {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin w-8 h-8 border-4 border-neutral-800 border-t-transparent rounded-full"></div>
</div>
)
}

View File

@@ -0,0 +1,32 @@
"use client";
import CreatePatientForm from "@/components/forms/patients/new/CreatePatientForm";
import SectionTitle from "@/components/SectionTitle";
import {useGetAllCountries} from "@/hooks/country";
import Link from "next/link";
import {useRouter} from "next/navigation";
import React from "react";
export default function Page() {
const router = useRouter();
const {data, isLoading} = useGetAllCountries();
return (
<div>
<div className="flex items-center justify-between mb-10">
<SectionTitle label="فرم افزودن بیمار جدید" />
<Link
href={`/patients`}
className="text-sm bg-primary px-4 py-2 rounded-lg text-white"
>
مشاهده لیست بیماران
</Link>
</div>
<CreatePatientForm
router={router}
countries={data?.data}
fetchCountriesLoading={isLoading}
/>
</div>
);
}

View File

@@ -0,0 +1,108 @@
import FilterByisDeleted from "@/components/FilterByisDeleted";
import Loader from "@/components/Loader";
import PatientsTable from "@/components/PatientsTable";
import RoleGuard from "@/components/RoleGuard";
import SearchBox from "@/components/SearchBox";
import {Field} from "@/components/ui/field";
import {Label} from "@/components/ui/label";
import UsersTableExport from "@/components/usersTableExport";
import {Plus} from "lucide-react";
import Link from "next/link";
import React, {Suspense} from "react";
export default function Page() {
return (
<>
<section>
<div className="grid grid-cols-4 gap-5">
<div className="col-span-3 flex items-center justify-start ">
<section className="w-full h-[250px]">
<h4 className="text-sm font-bold underline underline-offset-8 mb-2">
جستجوی پیشرفته
</h4>
<div className="grid grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-4 p-2">
<SearchBox
inputName="id"
hasLabel
label="شناسه"
route="patients"
/>
<SearchBox
inputName="name"
hasLabel
label="نام بیمار"
route="patients"
/>
<SearchBox
inputName="lastname"
hasLabel
label="نام خانوادگی"
route="patients"
/>
<SearchBox
inputName="ncode"
hasLabel
label="کدملی "
route="patients"
/>
<SearchBox
inputName="pcode"
hasLabel
label="کد پاسپورت "
route="patients"
/>
<SearchBox
inputName="phone"
hasLabel
label="شماره تماس "
route="patients"
/>
<SearchBox
inputName="email"
hasLabel
label="ایمیل بیمار"
route="patients"
/>
<SearchBox
inputName="age"
hasLabel
label="سن"
route="patients"
/>
<Field className="w-[300px] flex flex-row justify-start items-center gap-x-3 ">
<Label className="!w-fit">فیلتر بر اساس</Label>
<FilterByisDeleted />
</Field>
</div>
</section>
{/* <ExpertiseFilterBox /> */}
</div>
<div className="col-span-1 flex flex-col items-end justify-start gap-x-7 gap-y-4">
<RoleGuard roles={["ADMIN", "DEVELOPER", "COORDINATOR"]}>
<Link
href={"/patients/new"}
className="text-white bg-primary w-[240px] max-w-full py-2 px-3 rounded-lg flex items-center justify-center gap-x-2 text-sm"
>
<Plus size={"20"} />
<span>افزودن بیمار جدید</span>
</Link>
</RoleGuard>
<RoleGuard roles={["ADMIN", "DEVELOPER", "COORDINATOR"]}>
<UsersTableExport table="DOCTOR" />
</RoleGuard>
</div>
</div>
<div className="space-y-4 mt-10">
<Suspense fallback={<Loader />}>
<PatientsTable />
</Suspense>
</div>
</section>
</>
);
}

View File

@@ -0,0 +1,10 @@
import RestorePatientTable from '@/components/RestorePatientTable'
import React from 'react'
export default function Page() {
return (
<div>
<RestorePatientTable />
</div>
)
}

View File

@@ -0,0 +1,60 @@
"use client";
import Loader from "@/components/Loader";
import {Button} from "@/components/ui/button";
import {
useGetPrivacyPolicy,
useUpdatePrivacyPolicy,
} from "@/hooks/privacy-policy";
import {handleAxiosError} from "@/lib/utils";
import dynamic from "next/dynamic";
import React, {Suspense, useEffect, useRef, useState} from "react";
import {toast} from "react-toastify";
const RichTextEditor = dynamic(() => import("@/components/TextEditor"), {
ssr: false,
});
type RichTextEditorHandle = {
getContent: () => string;
};
export default function Page() {
const editorRef = useRef<RichTextEditorHandle>(null);
const [editorContent, setEditorContent] = useState<string>("");
const {mutateAsync} = useUpdatePrivacyPolicy();
const {data, isLoading} = useGetPrivacyPolicy();
const handleSave = async () => {
if (editorRef.current) {
const content = editorRef.current.getContent();
setEditorContent(content);
try {
const {message} = await mutateAsync(content);
toast.success(message);
} catch (error) {
toast.error(handleAxiosError(error));
}
}
};
useEffect(() => {
console.log(data)
setEditorContent(data?.data?.content);
}, [isLoading, data]);
return (
<div className="p-4">
<h1 className="text-center font-bold my-5 text-xl">متن Privacy Policy</h1>
<div>
{(data?.data || !isLoading) && (
<Suspense fallback={<Loader />}>
<RichTextEditor ref={editorRef} value={editorContent} />
</Suspense>
)}
</div>
<div className="mt-5">
<Button onClick={handleSave}>ثبت تغییرات</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,193 @@
"use client";
import CreateTosForm from "@/components/forms/tos/create/CreateTosForm";
import UpdateTosForm from "@/components/forms/tos/update/UpdateTosForm";
import Loader from "@/components/Loader";
import SectionTitle from "@/components/SectionTitle";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
useCreateTos,
useDeleteTos,
useGetAllTos,
useGetSingleTos,
useTosToggleActive,
} from "@/hooks/tos";
import { handleAxiosError } from "@/lib/utils";
import { useQueryClient } from "@tanstack/react-query";
import { Plus, Trash } from "lucide-react";
import { useRouter } from "next/navigation";
import React, { Suspense, useState } from "react";
import { toast } from "react-toastify";
export default function Page() {
const router = useRouter();
const [mode, setMode] = useState<"new" | "edit" | "form">("new");
const [version, setVersion] = useState<string>("");
const { mutateAsync: createAsync, isPending: createPending } = useCreateTos();
const queryClient = useQueryClient();
const { data: AllTos} = useGetAllTos();
const { mutateAsync: toggleAsync, isPending: togglePending } =
useTosToggleActive();
const { mutateAsync: deleteAsync, isPending: deletePending } = useDeleteTos();
const {
mutateAsync: updateAsync,
isPending: singleTosPending,
data: preValues,
} = useGetSingleTos();
const handleGetSingleTos = async (version: string) => {
try {
await updateAsync(version);
} catch (error) {
toast.error("adassd");
}
};
const handleToggleActive = async (version: string) => {
try {
const { message } = await toggleAsync(version);
await queryClient.invalidateQueries({ queryKey: ["get-all-tos"] });
toast.success(message);
} catch (error) {
toast.error(handleAxiosError(error));
}
};
const handleDeleteTos = async (version: string) => {
try {
const { message } = await deleteAsync(version);
await queryClient.invalidateQueries({ queryKey: ["get-all-tos"] });
toast.success(message);
} catch (error) {
toast.error(handleAxiosError(error));
}
};
return (
<div className="p-4">
<div className="flex items-center gap-x-4 w-full mb-10">
{mode === "new" && (
<Button
onClick={() => {
setMode("form");
setVersion("");
}}
>
<Plus />
ایجاد نسخه جدید
</Button>
)}
{mode === "form" && (
<>
<CreateTosForm
router={router}
createFn={createAsync}
isPending={createPending}
mode={mode}
setMode={setMode}
queryClient={queryClient}
/>
</>
)}
{mode === "edit" && version && preValues?.data && (
<Suspense fallback={<Loader />}>
<UpdateTosForm
router={router}
updateFn={updateAsync}
updatePending={singleTosPending}
mode={mode}
setMode={setMode}
queryClient={queryClient}
preValues={preValues?.data}
version={version}
setVersion={setVersion}
/>
</Suspense>
)}
</div>
<div className=" mb-10">
<SectionTitle label="ورژن های موجود" />
</div>
<div className="grid grid-cols-2 gap-y-4">
{AllTos &&
AllTos?.data?.length > 0 &&
AllTos?.data?.map((item, index) => (
<div
key={item?.id}
className="col-span-2 flex items-center justify-between gap-4 bg-white rounded-lg px-4 py-2 shadow-sm min-w-[200px]"
>
<div>
<span>ردیف : </span>
<span>{index + 1}</span>
</div>
<div className="rounded-lg p-4 flex items-center justify-start gap-x-2">
<span>عنوان : </span>
<span>{item.title}</span>
</div>
<div className="flex items-center justify-start shadow-inset gap-x-2 p-1">
<span>ورژن : </span>
<span>
<Badge variant={"secondary"}>{item.version}</Badge>
</span>
</div>
<div className="flex items-center justify-end gap-x-4">
<div>
{item.version === version && mode === "edit" ? (
<Button
type="button"
variant={"destructive"}
onClick={() => {
setVersion("");
setMode("new");
}}
>
انصراف
</Button>
) : (
<Button
disabled={mode === "form" || version ? true : false}
type="button"
variant={"outline"}
onClick={() => {
setVersion(item.version);
setMode("edit");
handleGetSingleTos(item.version);
}}
>
ویرایش
</Button>
)}
</div>
<div>
{item.isActive ? (
<Badge variant={"default"} className="bg-green-600">
فعال است
</Badge>
) : (
<Button
variant={"default"}
onClick={() => handleToggleActive(item.version)}
>
فعال کردن
</Button>
)}
</div>
<div>
<Button
// disabled={item.isActive || deletePending}
variant={item.isActive ? "secondary" : "destructive"}
onClick={() => handleDeleteTos(item.version)}
>
{deletePending ? <Loader size="4" /> : <Trash />}
</Button>
</div>
</div>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,28 @@
"use client";
import DenyAccess from "@/components/DenyAccess";
import CreateTransferPackageForm from "@/components/forms/transfer-packages/new/CreateTransferPackageForm";
import Loader from "@/components/Loader";
import { useGetLanguages } from "@/hooks/languages";
import React, { Suspense } from "react";
export default function Page() {
const { data } = useGetLanguages();
return (
<div>
<Suspense fallback={<Loader />}>
{data ? (
data?.data?.length > 0 ? (
<CreateTransferPackageForm
createPending={false}
languages={data?.data}
/>
) : (
<DenyAccess label="زبان" link="/languages" />
)
) : (
<div>خطا در دریافت دیتای زبان ها</div>
)}
</Suspense>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import React from 'react'
export default function Page() {
return (
<div>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Loading() {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin w-8 h-8 border-4 border-neutral-800 border-t-transparent rounded-full"></div>
</div>
)
}

View File

@@ -0,0 +1,67 @@
"use client";
import UpdateDoctorForm from "@/components/forms/doctor/update/UpdateDoctorForm";
import Loader from "@/components/Loader";
import SectionTitle from "@/components/SectionTitle";
import { useGetAllExpertiseList } from "@/hooks/expertise";
import { useGetLanguages } from "@/hooks/languages";
import {useGetSingleUser, useUpdateUser} from "@/hooks/users";
import Link from "next/link";
import {useParams, useRouter} from "next/navigation";
import {Suspense, useState} from "react";
export default function Page() {
const params = useParams();
const id = params?.id;
const router = useRouter();
const [loading] = useState(false);
const {
data: languages,
} = useGetLanguages();
const {
data: expertises,
} = useGetAllExpertiseList();
const {
data: singleUser,
} = useGetSingleUser(id?.toString() ?? "");
const {mutateAsync} = useUpdateUser();
const {data} = singleUser || {};
return (
<div>
<div className="flex items-center justify-between mb-10">
<SectionTitle label="فرم ویرایش عضو تیم " />
<div className="flex items-center justify-center gap-x-4">
<Link
href={`/transfer-team/members/new`}
className="text-sm bg-[#313131] px-4 py-2 rounded-lg text-white"
>
افزودن عضو جدید
</Link>
<Link
href={`/transfer-team/members`}
className="text-sm bg-primary px-4 py-2 rounded-lg text-white"
>
مشاهده لیست اعضاء تیم
</Link>
</div>
</div>
<Suspense fallback={<Loader />}>
{data && expertises && languages && !loading && (
<UpdateDoctorForm
expertises={expertises?.data}
router={router}
languages={languages?.data}
preValues={data}
id={id?.toString() ??""}
updateFn={mutateAsync}
/>
)}
</Suspense>
</div>
);
}

View File

@@ -0,0 +1,6 @@
"use client";
import {notFound} from "next/navigation";
export default function Page() {
return notFound();
}

View File

@@ -0,0 +1,105 @@
"use client";
import DenyAccess from "@/components/DenyAccess";
import CreateTransferTeamMember from "@/components/forms/transfer-team/member/new/CreateTransferTeamMemberForm";
import Loader from "@/components/Loader";
import SectionTitle from "@/components/SectionTitle";
import {API_URL} from "@/constants";
import {Expertise, Language} from "@/types";
import Link from "next/link";
import {useRouter} from "next/navigation";
import {Suspense, useEffect, useState} from "react";
async function fetchLanguages() {
const res = await fetch(`${API_URL}/language/get/all`,{cache:"no-cache"});
if (!res.ok && res.status==500) {
throw new Error("Failed to get data");
}
if(!res.ok && res.status==404){
return []
}
const data = await res.json();
return data;
}
async function fetchExpertise() {
const res = await fetch(`${API_URL}/expertise/fa/get/all/list`,{cache:"no-cache"});
if (!res.ok && res.status==500) {
throw new Error("Failed to get data");
}
if(!res.ok && res.status==404){
return []
}
const data = await res.json();
return data;
}
export default function Page() {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [data, setData] = useState<Language[]>([]);
const [expertises, setExpertises] = useState<Expertise[]>([]);
useEffect(() => {
let active = true;
setLoading(true);
fetchExpertise()
.then((res) => {
if (!active) return;
setExpertises(res.data);
})
.catch(console.error)
.finally(() => active && setLoading(false));
return () => {
active = false;
};
}, []);
useEffect(() => {
let active = true;
setLoading(true);
fetchLanguages()
.then((res) => {
if (!active) return;
setData(res.data);
})
.catch(console.error)
.finally(() => active && setLoading(false));
return () => {
active = false;
};
}, []);
if (loading) {
return <Loader />;
}
return (
<div>
<div className="flex items-center justify-between mb-10">
<SectionTitle label="فرم افزودن عضو جدید" />
<Link
href={`/transfer-team/members`}
className="text-sm bg-primary px-4 py-2 rounded-lg text-white"
>
مشاهده لیست اعضا
</Link>
</div>
<Suspense fallback={<Loader />}>
{data?.length > 0 ? expertises?.length > 0 ? <CreateTransferTeamMember
expertises={expertises}
router={router}
languages={data}
/> : <DenyAccess label="تخصص" link="/expertise/new"/> : <DenyAccess label="زبان" link="/languages"/> }
</Suspense>
</div>
);
}

View File

@@ -0,0 +1,49 @@
import Loader from "@/components/Loader";
import RoleGuard from "@/components/RoleGuard";
import SearchBox from "@/components/SearchBox";
import TransferTeamTable from "@/components/TransferTeamTable";
import UsersTableExport from "@/components/usersTableExport";
import {Plus} from "lucide-react";
import Link from "next/link";
import React, {Suspense} from "react";
export default function Page() {
return (
<>
<section>
<div className="flex items-center justify-between">
<div className="flex items-center justify-start gap-x-8">
<SearchBox
inputName="search"
hasLabel
label="نام خانوادگی"
placeholder="جستجو نام خانوادگی عضو تیم ..."
route="transfer-team"
/>
</div>
<div className="flex items-center justify-end gap-x-7">
<RoleGuard roles={["ADMIN","DEVELOPER"]}>
<Link
href={"/transfer-team/members/new"}
className="text-white bg-primary py-1.5 px-3 rounded-lg flex items-center gap-x-2 text-sm"
>
<Plus size={"20"} />
<span>افزودن عضو جدید</span>
</Link>
</RoleGuard>
<RoleGuard roles={["ADMIN","DEVELOPER"]}>
<UsersTableExport table="TRANSFER_TEAM" />
</RoleGuard>
</div>
</div>
<div className="space-y-4 mt-10">
<Suspense fallback={<Loader />}>
<TransferTeamTable />
</Suspense>
</div>
</section>
</>
);
}

View File

@@ -0,0 +1,41 @@
"use client";
import CreateTransferTeamForm from "@/components/forms/transfer-team/team/new/CreateTransferTeamForm";
import Loader from "@/components/Loader";
import SectionTitle from "@/components/SectionTitle";
import {useCreateTransferTeam, useGetAllTransferTeamAllMembers} from "@/hooks/transfer-team";
import Link from "next/link";
import {useRouter} from "next/navigation";
import React, {Suspense} from "react";
export default function Page() {
const {data: members, isLoading: fetchMembersLoading} =
useGetAllTransferTeamAllMembers();
const {mutateAsync,isPending}=useCreateTransferTeam()
const router = useRouter();
return (
<section>
<div className="flex items-center justify-between mb-10">
<SectionTitle label="فرم افزودن تیم جدید" />
<Link
href={`/transfer-team/teams`}
className="text-sm bg-primary px-4 py-2 rounded-lg text-white"
>
مشاهده لیست تیم ها
</Link>
</div>
<Suspense fallback={<Loader />}>
{members && (
<CreateTransferTeamForm
router={router}
fetchMembersLoading={fetchMembersLoading}
fetchPackagesLoading={false}
packages={[]}
members={members?.data}
createFn={mutateAsync}
createPending={isPending}
/>
)}
</Suspense>
</section>
);
}

View File

@@ -0,0 +1,38 @@
"use client";
import CreateTransferTeamForm from "@/components/forms/transfer-team/team/new/CreateTransferTeamForm";
import Loader from "@/components/Loader";
import SectionTitle from "@/components/SectionTitle";
import {useGetAllTransferTeamAllMembers} from "@/hooks/transfer-team";
import Link from "next/link";
import {useRouter} from "next/navigation";
import React, {Suspense} from "react";
export default function Page() {
const {data: members, isLoading: fetchMembersLoading} =
useGetAllTransferTeamAllMembers();
const router = useRouter();
return (
<section>
<div className="flex items-center justify-between mb-10">
<SectionTitle label="فرم افزودن تیم جدید" />
<Link
href={`/transfer-team/teams`}
className="text-sm bg-primary px-4 py-2 rounded-lg text-white"
>
مشاهده لیست تیم ها
</Link>
</div>
<Suspense fallback={<Loader />}>
{members && (
<CreateTransferTeamForm
router={router}
fetchMembersLoading={fetchMembersLoading}
fetchPackagesLoading={false}
packages={[]}
members={members}
/>
)}
</Suspense>
</section>
);
}

17
src/app/error.tsx Normal file
View File

@@ -0,0 +1,17 @@
"use client"
import Link from "next/link";
export default function ErrorPage() {
return (
<div className="flex flex-col items-center justify-center h-screen bg-linear-to-b bg-radial from-red-100 to-white text-red-800">
<h1 className="text-6xl font-bold mb-4">500</h1>
<p className="text-xl mb-6">خطای سرور رخ داده است.</p>
<Link
href="/"
className="px-6 py-3 bg-red-600 text-white rounded hover:bg-red-700 transition"
>
بازگشت به صفحه اصلی
</Link>
</div>
);
}

View File

@@ -1,26 +1,147 @@
@import "tailwindcss"; @import "tailwindcss";
@import "tw-animate-css";
:root { @custom-variant dark (&:is(.dark *));
--background: #ffffff;
--foreground: #171717;
}
@theme inline { @theme inline {
--color-background: var(--background); --color-background: var(--background);
--color-foreground: var(--foreground); --color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans); --font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono); --font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
--font-size-content: calc(var(--rem-size-2) * 1rem);
} }
@media (prefers-color-scheme: dark) { :root {
:root { --radius: 0.625rem;
--background: #0a0a0a; --background: #f6f6f6;
--foreground: #ededed; --foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.32 0.16 255);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--toastify-font-family: VazirMatn, var(--font-vazir);
--panel-width: 120px;
--panel-width-padding: 140px;
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50 font-normal;
}
body {
@apply bg-background text-foreground;
}
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
} }
} }
body { body {
background: var(--background); font-family: VazirMatn, Arial, Helvetica, sans-serif;
color: var(--foreground); scroll-behavior: smooth;
font-family: Arial, Helvetica, sans-serif; }
.custom-toast {
font-family: VazirMatn, sans-serif; /* فونت فارسی دلخواه */
font-size: 16px;
}
.ql-editor {
font-family: VazirMatn, sans-serif !important;
font-size: 16px;
line-height: 1.6;
} }

View File

@@ -1,19 +1,10 @@
import type { Metadata } from "next"; import type {Metadata} from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
import {FontVazir} from "@/config/font.config";
const geistSans = Geist({ import {ToastContainer} from "react-toastify";
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Create Next App", title: "پنل مدیریت وب سایت بیماران بین الملل",
description: "Generated by create next app", description: "Generated by create next app",
}; };
@@ -23,11 +14,22 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="en"> <html lang="en" dir="rtl">
<body <body
className={`${geistSans.variable} ${geistMono.variable} antialiased`} className={`${FontVazir.variable} antialiased`}
style={{fontFamily: FontVazir.style.fontFamily}}
> >
{children} {children}
<ToastContainer
style={{fontFamily: FontVazir.style.fontFamily,fontSize:'14px'}}
position="top-right"
autoClose={3000}
hideProgressBar={false}
newestOnTop
closeOnClick
pauseOnHover
draggable
/>
</body> </body>
</html> </html>
); );

16
src/app/not-found.tsx Normal file
View File

@@ -0,0 +1,16 @@
import Link from "next/link";
export default function NotFoundPage() {
return (
<div className="flex flex-col items-center justify-center h-screen bg-gray-50 text-gray-800">
<h1 className="text-6xl font-bold mb-4">404</h1>
<p className="text-xl mb-6">صفحهای که دنبال آن هستید پیدا نشد.</p>
<Link
href="/dashboard"
className="px-6 py-3 bg-blue-600 text-white rounded hover:bg-blue-700 transition"
>
بازگشت به صفحه اصلی
</Link>
</div>
);
}

View File

@@ -1,103 +1,47 @@
import { LogIn } from "lucide-react";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link";
export default function Home() { export default function Home() {
return ( return (
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20"> <div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start"> <main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-center">
<Image <Image
className="dark:invert" className="dark:invert"
src="/next.svg" src="/logo.png"
alt="Next.js logo" alt="Shomal Amol Hospital"
width={180} width={280}
height={38} height={48}
priority priority
/> />
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left"> <h4 className="mb-1 tracking-[-.01em] font-bold text-xl">
<li className="mb-2 tracking-[-.01em]"> پنل مدیریت سایت بیماران بین الملل
Get started by editing{" "} </h4>
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded"> {/* <p className="tracking-[-.01em]">
src/app/page.tsx یکی از دکمه های زیر را فشار دهید
</code> </p> */}
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row"> <div className="flex gap-4 items-center flex-col sm:flex-row">
<a <Link
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto" className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" href="/dashboard"
target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<Image <LogIn />
className="dark:invert" ورود به پنل
src="/vercel.svg" </Link>
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a <a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]" className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[200px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" href="https://ipd.shomalhospital.com"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
Read our docs مشاهده وب سایت
</a> </a>
</div> </div>
</main> </main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div> </div>
); );
} }

View File

@@ -0,0 +1,27 @@
"use client";
import {useMe} from "@/hooks";
import Loader from "./Loader";
export default function AuthGuard({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const {isLoading} = useMe();
if (isLoading)
return (
<div className="h-screen flex items-center justify-center w-full">
<div className="flex items-center justify-center gap-x-4 mx-auto">
<span>لطفا منتظر بمانید</span>
<span>
<Loader />
</span>
</div>
</div>
);
return children;
}

View File

@@ -0,0 +1,49 @@
"use client";
import * as React from "react";
import {ChevronDownIcon} from "lucide-react";
import {Button} from "@/components/ui/button";
import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover";
import {CalendarHijriBase} from "./ui/calendar-hijri";
export function CalendarHijri({
date,
setDate,
}: {
date: Date | undefined;
setDate: React.Dispatch<React.SetStateAction<Date | undefined>>;
}) {
const [open, setOpen] = React.useState(false);
return (
<div className="flex flex-col gap-3 w-full">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild className="w-full bg-white">
<Button
variant="outline"
id="date"
className="w-full justify-between font-normal"
>
{date ? date.toLocaleDateString("fa-IR") : "Select date"}
<ChevronDownIcon />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-full overflow-hidden p-0 bg-white"
align="start"
>
<CalendarHijriBase
mode="single"
selected={date}
captionLayout="dropdown"
onSelect={(date) => {
setDate(date);
setOpen(false);
}}
/>
</PopoverContent>
</Popover>
</div>
);
}

View File

@@ -0,0 +1,49 @@
"use client";
import * as React from "react";
import {ChevronDownIcon} from "lucide-react";
import {Button} from "@/components/ui/button";
import {Calendar} from "@/components/ui/calendar";
import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover";
export function CalendarNormal({
date,
setDate,
}: {
date: Date | undefined;
setDate: React.Dispatch<React.SetStateAction<Date | undefined>>;
}) {
const [open, setOpen] = React.useState(false);
return (
<div className="flex flex-col gap-3 w-full">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild className="bg-white">
<Button
variant="outline"
id="date"
className="w-full justify-between font-normal"
>
{date ? date.toLocaleDateString() : "Select date"}
<ChevronDownIcon />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-auto overflow-hidden p-0 bg-white"
align="start"
>
<Calendar
mode="single"
selected={date}
captionLayout="dropdown"
onSelect={(date) => {
setDate(date);
setOpen(false);
}}
/>
</PopoverContent>
</Popover>
</div>
);
}

View File

@@ -0,0 +1,184 @@
import {API_CDN_URL} from "@/constants";
import {formatSystemForUI} from "@/lib/utils";
import React from "react";
import SectionTitle from "./SectionTitle";
import Link from "next/link";
import { Logs } from "lucide-react";
async function getCDNStatus() {
try {
const res = await fetch(`${API_CDN_URL}/status`, {
method: "GET",
credentials: "include",
});
if (!res.ok) {
const errorData = await res.json().catch(() => null);
return {
status: "SERVICE_ERROR",
message: errorData?.message || "سرویس با خطا پاسخ داد",
system: null,
};
}
const data = await res.json();
return {
status: "OK",
message: data.message,
system: data.system,
};
} catch {
return {
status: "DOWN",
message: "سرویس پاسخگو نیست",
system: null,
};
}
}
export default async function DashboardCDNServerStatus() {
const data = await getCDNStatus();
const {status, message, system} = data || {};
const uiSystem = system ? formatSystemForUI(system) : null;
return (
<section className="rounded-lg border-[1px] border-neutral-200 p-10 bg-white">
<>
<div className="flex items-center gap-x-4 mb-10">
<SectionTitle label="وضعیت سرویس آپلود" />
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-[#313131]">وضعیت</span>
<div className="flex items-center gap-x-2">
<span className="relative flex size-3">
<span
className={`absolute inline-flex h-full w-full animate-ping rounded-full ${
status === "OK" ? "bg-green-400" : "bg-red-400"
} opacity-75`}
></span>
<span
className={`relative inline-flex size-3 rounded-full ${
status === "OK" ? "bg-green-500" : "bg-red-500"
}`}
></span>
</span>
<span className="text-xs font-semibold">
{message} ({status})
</span>
</div>
</div>
</>
{uiSystem && (
<>
<div className="flex items-center justify-between gap-x-4 my-10">
<SectionTitle label="مشخصات سرویس" />
</div>
<div className="space-y-4" dir="ltr">
<div className="flex items-center justify-between">
<span className="text-sm text-[#313131]">uptime</span>
<div className="flex items-center gap-x-2">
<span className="text-xs font-semibold">{uiSystem.uptime}</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-[#313131]">pid</span>
<div className="flex items-center gap-x-2">
<span className="text-xs font-semibold">{uiSystem.pid}</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-[#313131]">node version</span>
<div className="flex items-center gap-x-2">
<span className="text-xs font-semibold">
{uiSystem.nodeVersion}
</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-[#313131]">heap used</span>
<div className="flex items-center gap-x-2">
<span className="text-xs font-semibold">
{uiSystem.heapUsed}
</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-[#313131]">heap total</span>
<div className="flex items-center gap-x-2">
<span className="text-xs font-semibold">
{uiSystem.heapTotal}
</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-[#313131]">rss</span>
<div className="flex items-center gap-x-2">
<span className="text-xs font-semibold">{uiSystem.rss}</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-[#313131]">cpu user</span>
<div className="flex items-center gap-x-2">
<span className="text-xs font-semibold">
{uiSystem.cpuUser}
</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-[#313131]">cpu system</span>
<div className="flex items-center gap-x-2">
<span className="text-xs font-semibold">
{uiSystem.cpuSystem}
</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-[#313131]">
event loop utilization
</span>
<div className="flex items-center gap-x-2">
<span className="text-xs font-semibold">
{uiSystem.eventLoopUtil}
</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-[#313131]">free memory</span>
<div className="flex items-center gap-x-2">
<span className="text-xs font-semibold">
{uiSystem.freeMemory}
</span>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-[#313131]">total memory</span>
<div className="flex items-center gap-x-2">
<span className="text-xs font-semibold">
{uiSystem.totalMemory}
</span>
</div>
</div>
</div>
<div className="flex w-full items-center justify-center mt-10">
<Link href={"/logs/upload-server"} className="rounded-lg flex items-center justify-center gap-x-4 text-sm font-semibold bg-primary text-white w-full py-4 text-center">
<span><Logs /></span>
<span>مشاهده لاگ ها</span>
</Link>
</div>
</>
)}
</section>
);
}

View File

@@ -0,0 +1,136 @@
import React from "react";
import SectionTitle from "./SectionTitle";
import {API_URL} from "@/constants";
import {cookies} from "next/headers";
export interface DashboardCounts {
department: {
members: number;
};
patients: number;
onlineCases: {
completed: number;
pending: number;
};
staff: {
coordinators: number;
admins: number;
doctors: number;
};
}
async function getData() {
try {
const res = await fetch(`${API_URL}/statistics/data`, {
method: "GET",
credentials: "include",
cache: "no-cache",
headers: {
Cookie: (await cookies()).toString(),
},
});
const data = await res.json();
return data;
} catch {
// throw new Error("failed to get data");
return {}
}
}
export default async function DashboardStatistics() {
const fetchedData = await getData();
const {data} = fetchedData || {};
return (
<>
<section className="bg-white border border-neutral-200 p-10 rounded-lg space-y-8">
<div className="space-y-6">
<SectionTitle label="آمار و اطلاعات دپارتمان" />
<div className="flex items-center flex-wrap justify-start gap-6 mt-10">
{data?.department && (
<>
<div className="border border-neutral-200 p-4 inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-all rounded-lg">
<span className="text-sm font-semibold">اعضای دپارتمان</span>
<span className="text-sm">
{Number(data?.department?.members).toLocaleString("fa-IR")}
</span>
</div>
<div className="border border-neutral-200 p-4 inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-all rounded-lg">
<span className="text-sm font-semibold">پزشکان دپارتمان</span>
<span className="text-sm">
{Number(data?.department?.doctors).toLocaleString("fa-IR")}
</span>
</div>
</>
)}
</div>
</div>
<div className="space-y-6">
<SectionTitle label="آمار و اطلاعات کاربران سایت" />
<div className="flex items-center flex-wrap justify-start gap-6 mt-10">
{data?.staff && (
<>
<div className="border border-neutral-200 p-4 inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-all rounded-lg">
<span className="text-sm font-semibold">کارشناسان سایت</span>
<span className="text-sm">
{Number(data?.staff?.coordinators).toLocaleString("fa-IR")}
</span>
</div>
<div className="border border-neutral-200 p-4 inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-all rounded-lg">
<span className="text-sm font-semibold">ادمین های سایت </span>
<span className="text-sm">
{Number(data?.staff?.admins).toLocaleString("fa-IR")}
</span>
</div>
<div className="border border-neutral-200 p-4 inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-all rounded-lg">
<span className="text-sm font-semibold">پزشکان سایت</span>
<span className="text-sm">
{Number(data?.staff?.doctors).toLocaleString("fa-IR")}
</span>
</div>
</>
)}
</div>
</div>
<div className="space-y-6">
<SectionTitle label="آمار و اطلاعات درمانی" />
<div className="flex items-center flex-wrap justify-start gap-6 mt-10">
{data?.patients && (
<div className="border border-neutral-200 p-4 inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-all rounded-lg">
<span className="text-sm font-semibold">تعداد کل بیماران</span>
<span className="text-sm">
{Number(data?.patients?.numbers).toLocaleString("fa-IR")}
</span>
</div>
)}
{data?.onlineCases && (
<>
<div className="border border-neutral-200 p-4 inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-all rounded-lg">
<span className="text-sm font-semibold">
{" "}
پذیرش های کامل شده
</span>
<span className="text-sm">
{Number(data?.onlineCases?.completed).toLocaleString(
"fa-IR"
)}
</span>
</div>
<div className="border border-neutral-200 p-4 inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-all rounded-lg">
<span className="text-sm font-semibold">
{" "}
پذیرش های درحال انجام
</span>
<span className="text-sm">
{Number(data?.onlineCases?.pending).toLocaleString("fa-IR")}
</span>
</div>
</>
)}
</div>
</div>
</section>
</>
);
}

View File

@@ -0,0 +1,67 @@
"use client";
import React from "react";
import {Button} from "./ui/button";
import {Trash} from "lucide-react";
import privateApi from "@/service/http/privateCall.axios";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "./ui/dialog";
import {toast} from "react-toastify";
import {handleAxiosError} from "@/lib/utils";
import { ServerResponseObject } from "@/types";
async function deleteUser(id: number | string, route: string) {
try {
const {message} = await privateApi.delete(`/${route}/delete/${id}`) as ServerResponseObject;
toast.success(message);
} catch (error) {
toast.error(handleAxiosError(error));
}
}
export default function DeleteExpertiseButton({
id,
route,
}: {
id: number | string;
route: string;
}) {
const handleDelete = async () => {
await deleteUser(id, route);
};
return (
<>
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive">
<Trash />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]" dir="rtl">
<DialogHeader>
<DialogTitle>حذف آیتم</DialogTitle>
<DialogDescription>
آیا از حذف این آیتم اطمینان دارد ؟
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">انصراف</Button>
</DialogClose>
<Button onClick={handleDelete} type="submit">
بله
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}

View File

@@ -0,0 +1,40 @@
"use client";
import React from "react";
import {Button} from "./ui/button";
import {Trash} from "lucide-react";
import {useDeleteFile} from "@/hooks";
import {handleAxiosError} from "@/lib/utils";
import {toast} from "react-toastify";
export default function DeleteFileButton({
type,
fileKey,
fileUrl,
}: {
type: "image" | "document";
fileKey: string;
fileUrl: string;
}) {
const {mutateAsync} = useDeleteFile();
const handleDeleteThumbnail = async () => {
try {
const {message} = await mutateAsync({fileKey, fileUrl, type});
toast.success(message);
} catch (error) {
toast.error(handleAxiosError(error));
}
};
return (
<>
<Button
type="button"
variant={"destructive"}
className="text-white absolute top-2 left-2 z-10"
onClick={handleDeleteThumbnail}
>
<Trash />
</Button>
</>
);
}

View File

@@ -0,0 +1,69 @@
"use client";
import React from "react";
import {Button} from "./ui/button";
import {Trash} from "lucide-react";
import privateApi from "@/service/http/privateCall.axios";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "./ui/dialog";
import {useQueryClient} from "@tanstack/react-query";
import {toast} from "react-toastify";
import {handleAxiosError} from "@/lib/utils";
import { ServerResponseObject } from "@/types";
async function deleteUser(id: number | string, route: string) {
try {
const {message} = await privateApi.delete(`/${route}/delete/${id}`) as ServerResponseObject;
toast.success(message);
} catch (error) {
toast.error(handleAxiosError(error));
}
}
export default function DeleteLanguageButton({
id,
route,
}: {
id: number | string;
route: string;
}) {
const queryClient = useQueryClient();
const handleDelete = async () => {
await deleteUser(id, route);
queryClient.invalidateQueries({queryKey: ["get-all-languages"]});
};
return (
<>
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive">
<Trash />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]" dir="rtl">
<DialogHeader>
<DialogTitle>حذف آیتم</DialogTitle>
<DialogDescription>
آیا از حذف این آیتم اطمینان دارد ؟
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">انصراف</Button>
</DialogClose>
<Button onClick={handleDelete} type="submit">
بله
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}

View File

@@ -0,0 +1,69 @@
"use client";
import React from "react";
import {Button} from "./ui/button";
import {Trash} from "lucide-react";
import privateApi from "@/service/http/privateCall.axios";
import {usePathname} from "next/navigation";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "./ui/dialog";
import {toast} from "react-toastify";
import {handleAxiosError} from "@/lib/utils";
import { ServerResponseObject } from "@/types";
async function deleteMedicalPackage(id: number | string, route: string) {
try {
const {message} = await privateApi.delete(`/${route}/delete/${id}`) as ServerResponseObject;
toast.success(message);
} catch (error) {
toast.error(handleAxiosError(error));
}
}
export default function DeleteMedicalPackageButton({
id,
route,
}: {
id: number | string;
route: string;
}) {
const pathname = usePathname();
const handleDelete = async () => {
await deleteMedicalPackage(id, route);
window.location.pathname = pathname;
};
return (
<>
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive">
<Trash />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]" dir="rtl">
<DialogHeader>
<DialogTitle>حذف آیتم</DialogTitle>
<DialogDescription>
آیا از حذف این آیتم اطمینان دارد ؟
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">انصراف</Button>
</DialogClose>
<Button onClick={handleDelete} type="submit">
بله
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}

View File

@@ -0,0 +1,8 @@
import Link from 'next/link'
import React from 'react'
export default function DenyAccess({label,link}:{label:string,link:string}) {
return (
<div className="bg-white rounded-4xl flex items-center justify-center p-10">شما نمیتوانید از این بخش استفاده کنید. ابتدا می بایست حداقل یک {label} ایجاد کنید. برای ایجاد روی &nbsp; <Link href={link} className="underline underline-offset-4"> لینک </Link> &nbsp; کلیک کنید</div>
)
}

View File

@@ -0,0 +1,260 @@
"use client";
import * as React from "react";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import {API_URL} from "@/constants";
import Loader from "./Loader";
import {useSearchParams} from "next/navigation";
import {Pencil} from "lucide-react";
import DeleteUserButton from "./deleteUserButton";
import Link from "next/link";
import RoleGuard from "./RoleGuard";
// =====================
// Types
// =====================
export type ApiResponse<T> = {
status: number;
message: string;
data: {
data: T[];
page: string;
limit: string;
total: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
};
};
type Translation = {
displayName: string;
lang: string;
};
type Expertise = {
slug: string;
translations: Translation[];
};
type UserTranslation = {
firstName: string;
lastName: string;
lang: string;
position: string;
};
export type Data = {
id: number;
email: string;
phone: string;
slug: string;
image: string | null;
translations: UserTranslation[];
expertise: Expertise;
};
async function fetchData(
page: number,
pageSize: number,
queries?: string,
): Promise<ApiResponse<Data>> {
const res = await fetch(
`${API_URL}/user/get/all?t=department&lang=fa&page=${page}&limit=${pageSize}${
queries ? `&${queries}` : ""
}`,
{
cache: "no-store",
credentials: "include",
},
);
if (!res.ok) {
throw new Error("Failed to fetch data");
}
return res.json();
}
export default function DepartmentMembersTable() {
const [page, setPage] = React.useState(1);
const [pageSize] = React.useState(20);
const [data, setData] = React.useState<Data[]>([]);
const [total, setTotal] = React.useState(0);
const [loading, setLoading] = React.useState(false);
const searchParams = useSearchParams();
const totalPages = Math.ceil(total / pageSize);
React.useEffect(() => {
let active = true;
setLoading(true);
fetchData(page, pageSize, searchParams.toString())
.then((res) => {
if (!active) return;
setData(res.data?.data);
setTotal(res.data?.total);
})
.catch(console.error)
.finally(() => active && setLoading(false));
return () => {
active = false;
};
}, [page, pageSize, searchParams]);
if (loading) {
return (
<div className="text-sm text-muted-foreground">
<Loader />
</div>
);
}
return (
<>
<div className="grid gap-4"></div>
<div className="overflow-x-auto " dir="rtl">
<table className="min-w-full border border-gray-200 rounded-lg bg-white">
<thead className="bg-gray-100 hidden md:table-header-group">
<tr>
<th className="px-4 py-2 text-center text-sm font-semibold">
نام
</th>
<th className="px-4 py-2 text-center text-sm font-semibold">
نام خانوادگی
</th>
<th className="px-4 py-2 text-center text-sm font-semibold">
سمت
</th>
<th className="px-4 py-2 text-center text-sm font-semibold">
تخصص
</th>
<th className="px-4 py-2 text-center text-sm font-semibold">
ایمیل
</th>
<th className="px-4 py-2 text-center text-sm font-semibold">
موبایل
</th>
<th className="px-4 py-2 text-center text-sm font-semibold">
اکشن ها
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-400">
{data.map((post) => (
<tr
key={post.id}
className="block md:table-row border-b md:border-none"
>
<td className="px-4 py-6 block md:table-cell text-center text-sm border-b border-b-neutral-200">
<span className="md:hidden font-medium text-gray-500">
نام
</span>
{post?.translations[0]?.firstName}
</td>
<td className="px-4 py-6 block md:table-cell text-center text-sm border-b border-b-neutral-200">
<span className="md:hidden font-medium text-gray-500">
نام خانوادگی
</span>
{post?.translations[0]?.lastName}
</td>
<td className="px-4 py-6 block md:table-cell text-center text-sm border-b border-b-neutral-200">
<span className="md:hidden font-medium text-gray-500">
سمت
</span>
{post.translations[0]?.position}
</td>
<td className="px-4 py-6 block md:table-cell text-center text-sm border-b border-b-neutral-200">
<span className="md:hidden font-medium text-gray-500">
تخصص
</span>
{post?.expertise?.translations[0]?.displayName}
</td>
<td className="px-4 py-6 block md:table-cell text-center text-sm border-b border-b-neutral-200">
<span className="md:hidden font-medium text-gray-500">
ایمیل
</span>
{post.email}
</td>
<td className="px-4 py-6 block md:table-cell text-center border-b border-b-neutral-200">
<span className="md:hidden font-medium text-gray-500">
موبایل
</span>
{post?.phone.toString().toLocaleLowerCase("fa-IR")}
</td>
<td className="px-4 py-6 block md:table-cell text-center border-b border-b-neutral-200">
<span className="md:hidden font-medium text-gray-500">
اکشن ها
</span>
<div className="flex items-center justify-center gap-x-4">
<RoleGuard roles={["ADMIN", "DEVELOPER"]}>
<DeleteUserButton route="user" id={post.id} />
<Link
className="bg-primary text-white p-1.5 rounded-md"
href={`/department/members/edit/${post.id}`}
>
<Pencil />
</Link>
</RoleGuard>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{total > 0 && (
<Pagination dir="ltr">
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={() => setPage((p) => Math.max(1, p - 1))}
className={
page === 1
? "pointer-events-none opacity-50"
: "cursor-pointer"
}
/>
</PaginationItem>
{Array.from({length: totalPages}).map((_, i) => {
const pageNumber = i + 1;
return (
<PaginationItem key={pageNumber}>
<PaginationLink
isActive={page === pageNumber}
onClick={() => setPage(pageNumber)}
className="cursor-pointer"
>
{pageNumber}
</PaginationLink>
</PaginationItem>
);
})}
<PaginationItem>
<PaginationNext
title="بعدی"
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
className={
page === totalPages
? "pointer-events-none opacity-50"
: "cursor-pointer"
}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)}
</>
);
}

View File

@@ -0,0 +1,261 @@
"use client";
import * as React from "react";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import {API_URL} from "@/constants";
import Loader from "./Loader";
import {useRouter, useSearchParams} from "next/navigation";
import {Pencil} from "lucide-react";
import DeleteUserButton from "./deleteUserButton";
import Link from "next/link";
import RoleGuard from "./RoleGuard";
export type ApiResponse<T> = {
status: number;
message: string;
data: {
data: T[];
page: string;
limit: string;
total: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
};
};
type Translation = {
displayName: string;
lang: string;
};
type Expertise = {
slug: string;
translations: Translation[];
};
type UserTranslation = {
firstName: string;
lastName: string;
lang: string;
position: string;
};
export type Data = {
id: number;
email: string;
phone: string;
slug: string;
image: string | null;
translations: UserTranslation[];
expertise: Expertise;
};
async function fetchData(
page: number,
pageSize: number,
queries?: string,
): Promise<ApiResponse<Data>> {
console.log("doctors table");
const res = await fetch(
`${API_URL}/user/get/all?t=doctor&lang=fa&page=${page}&limit=${pageSize}${
queries ? `&${queries}` : ""
}`,
{
cache: "no-store",
credentials: "include",
},
);
if (!res.ok) {
throw new Error("Failed to fetch data");
}
return res.json();
}
export default function DoctorsTable() {
const [page, setPage] = React.useState(1);
const [pageSize] = React.useState(20);
const [data, setData] = React.useState<Data[]>([]);
const [total, setTotal] = React.useState(0);
const [loading, setLoading] = React.useState(false);
const searchParams = useSearchParams();
const totalPages = Math.ceil(total / pageSize);
const router = useRouter();
React.useEffect(() => {
let active = true;
setLoading(true);
fetchData(page, pageSize, searchParams.toString())
.then((res) => {
if (!active) return;
setData(res.data?.data);
setTotal(res.data?.total);
})
.catch(console.error)
.finally(() => active && setLoading(false));
return () => {
active = false;
};
}, [page, pageSize, searchParams]);
if (loading) {
return (
<div className="text-sm text-muted-foreground">
<Loader />
</div>
);
}
return (
<>
<div className="grid gap-4"></div>
<div className="overflow-x-auto " dir="rtl">
<table className="min-w-full border border-gray-200 rounded-lg bg-white">
<thead className="bg-gray-100 hidden md:table-header-group">
<tr>
<th className="px-4 py-2 text-center text-sm font-semibold">
نام
</th>
<th className="px-4 py-2 text-center text-sm font-semibold">
نام خانوادگی
</th>
<th className="px-4 py-2 text-center text-sm font-semibold">
سمت
</th>
<th className="px-4 py-2 text-center text-sm font-semibold">
تخصص
</th>
<th className="px-4 py-2 text-center text-sm font-semibold">
ایمیل
</th>
<th className="px-4 py-2 text-center text-sm font-semibold">
موبایل
</th>
<th className="px-4 py-2 text-center text-sm font-semibold">
اکشن ها
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-400">
{data.map((post) => (
<tr
key={post.id}
className="block md:table-row border-b md:border-none"
>
<td className="px-4 py-6 block md:table-cell text-center text-sm border-b border-b-neutral-200">
<span className="md:hidden font-medium text-gray-500">
نام
</span>
{post?.translations[0]?.firstName}
</td>
<td className="px-4 py-6 block md:table-cell text-center text-sm border-b border-b-neutral-200">
<span className="md:hidden font-medium text-gray-500">
نام خانوادگی
</span>
{post?.translations[0]?.lastName}
</td>
<td className="px-4 py-6 block md:table-cell text-center text-sm border-b border-b-neutral-200">
<span className="md:hidden font-medium text-gray-500">
سمت
</span>
{post.translations[0]?.position}
</td>
<td className="px-4 py-6 block md:table-cell text-center text-sm border-b border-b-neutral-200">
<span className="md:hidden font-medium text-gray-500">
تخصص
</span>
{post?.expertise?.translations[0]?.displayName}
</td>
<td className="px-4 py-6 block md:table-cell text-center text-sm border-b border-b-neutral-200">
<span className="md:hidden font-medium text-gray-500">
ایمیل
</span>
{post.email}
</td>
<td className="px-4 py-6 block md:table-cell text-center border-b border-b-neutral-200">
<span className="md:hidden font-medium text-gray-500">
موبایل
</span>
{post?.phone.toString().toLocaleLowerCase("fa-IR")}
</td>
<td className="px-4 py-6 block md:table-cell text-center border-b border-b-neutral-200">
<span className="md:hidden font-medium text-gray-500">
اکشن ها
</span>
<div className="flex items-center justify-center gap-x-4">
<RoleGuard roles={["ADMIN", "DEVELOPER"]}>
<DeleteUserButton id={post.id} route="user" />
</RoleGuard>
<RoleGuard roles={["ADMIN", "DEVELOPER", "COORDINATOR"]}>
<Link
className="bg-primary text-white p-1.5 rounded-md"
href={`/doctors/edit/${post.id}`}
>
<Pencil />
</Link>
</RoleGuard>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{total > 0 && (
<Pagination dir="ltr">
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={() => setPage((p) => Math.max(1, p - 1))}
className={
page === 1
? "pointer-events-none opacity-50"
: "cursor-pointer"
}
/>
</PaginationItem>
{Array.from({length: totalPages}).map((_, i) => {
const pageNumber = i + 1;
return (
<PaginationItem key={pageNumber}>
<PaginationLink
isActive={page === pageNumber}
onClick={() => setPage(pageNumber)}
className="cursor-pointer"
>
{pageNumber}
</PaginationLink>
</PaginationItem>
);
})}
<PaginationItem>
<PaginationNext
title="بعدی"
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
className={
page === totalPages
? "pointer-events-none opacity-50"
: "cursor-pointer"
}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)}
</>
);
}

View File

@@ -0,0 +1,94 @@
"use client";
import React from "react";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "./ui/select";
import {useRouter, useSearchParams} from "next/navigation";
import {API_URL} from "@/constants";
import Loader from "./Loader";
interface Data {
id: number;
slug: string;
translations: {
id: number;
displayName: string;
lang: string;
}[];
}
async function fetchData() {
console.log('filters box')
const res = await fetch(`${API_URL}/expertise/fa/get/all/list?lang=fa`, {
cache: "no-store",
credentials: "include",
});
if (!res.ok) {
throw new Error("Failed to fetch data");
}
const data = await res.json();
return data;
}
export default function ExpertiseFilterBox() {
const urlSearchParams = useSearchParams();
const [data, setData] = React.useState([]);
const [loading, setLoading] = React.useState(false);
const searchParams = new URLSearchParams(urlSearchParams);
const router = useRouter();
React.useEffect(() => {
let active = true;
setLoading(true);
fetchData()
.then((res) => {
if (!active) return;
setData(res.data);
})
.catch(console.error)
.finally(() => active && setLoading(false));
return () => {
active = false;
};
}, []);
const handleFilter = (value: string) => {
searchParams.set("e", value);
router.push(`/doctors${searchParams ? `?${searchParams.toString()}` : ""}`);
};
if (loading) {
return (
<div className="text-sm text-muted-foreground">
<Loader />
</div>
);
}
return (
<>
<Select onValueChange={handleFilter}>
<SelectTrigger className="w-[180px] bg-white" dir="rtl">
<SelectValue placeholder="تخصص انتخاب کنید" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{data?.map((item: Data) => (
<SelectItem key={item.id} value={item.slug}>
{item.translations[0].displayName}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</>
);
}

View File

@@ -0,0 +1,234 @@
"use client";
import * as React from "react";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import {API_URL} from "@/constants";
import Loader from "./Loader";
import {useSearchParams} from "next/navigation";
import {Pencil} from "lucide-react";
import Link from "next/link";
import RoleGuard from "./RoleGuard";
import DeleteExpertiseButton from "./DeleteExpertiseButton";
export type ApiResponse<T> = {
status: number;
message: string;
data: {
data: T[];
page: string;
limit: string;
total: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
};
};
export type Data = {
slug: string;
id: number;
translations: {
displayName: string | null;
lang: {
title: string;
} | null;
}[];
};
async function fetchData(
lang: string,
page: number,
pageSize: number,
queries?: string,
): Promise<ApiResponse<Data>> {
console.log("doctors table");
const res = await fetch(
`${API_URL}/expertise/${
lang ?? "fa"
}/get/all?page=${page}&limit=${pageSize}${queries ? `&${queries}` : ""}`,
{
cache: "no-store",
credentials: "include",
},
);
if (!res.ok) {
throw new Error("Failed to fetch data");
}
return await res.json();
}
export default function ExpertiseTable({lang}: {lang: string}) {
const [page, setPage] = React.useState(1);
const [pageSize] = React.useState(20);
const [data, setData] = React.useState<Data[]>([]);
const [total, setTotal] = React.useState(0);
const [loading, setLoading] = React.useState(false);
const searchParams = useSearchParams();
const totalPages = Math.ceil(total / pageSize);
React.useEffect(() => {
let active = true;
setLoading(true);
fetchData(lang, page, pageSize, searchParams.toString())
.then((res) => {
if (!active) return;
console.log(res.data);
setData(res.data?.data);
setTotal(res.data?.total);
})
.catch(console.error)
.finally(() => active && setLoading(false));
return () => {
active = false;
};
}, [page, pageSize, searchParams, lang]);
if (loading) {
return (
<div className="text-sm text-muted-foreground">
<Loader />
</div>
);
}
return (
<>
<div className="grid gap-4"></div>
<div className="overflow-x-auto " dir="rtl">
<table className="min-w-full border border-gray-200 rounded-lg bg-white">
<thead className="bg-gray-100 hidden md:table-header-group">
<tr>
<th className="px-4 py-2 text-center text-sm font-semibold">
ردیف
</th>
<th className="px-4 py-2 text-center text-sm font-semibold">
اسلاگ
</th>
<th className="px-4 py-2 text-center text-sm font-semibold">
نام
</th>
<th className="px-4 py-2 text-center text-sm font-semibold">
نام زبان
</th>
<th className="px-4 py-2 text-center text-sm font-semibold">
اکشن ها
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-400">
{data.map((post, index) => (
<tr
key={post.id}
className="block md:table-row border-b md:border-none"
>
<td className="px-4 py-6 block md:table-cell text-center text-sm border-b border-b-neutral-200">
<span className="md:hidden font-medium text-gray-500">
ردیف
</span>
{index + 1}
</td>
<td className="px-4 py-6 block md:table-cell text-center text-sm border-b border-b-neutral-200">
<span className="md:hidden font-medium text-gray-500">
اسلاگ
</span>
{post?.slug}
</td>
<td className="px-4 py-6 block md:table-cell text-center text-sm border-b border-b-neutral-200">
<span className="md:hidden font-medium text-gray-500">
نام
</span>
{post?.translations[0]?.displayName}
</td>
<td className="px-4 py-6 block md:table-cell text-center text-sm border-b border-b-neutral-200">
<span className="md:hidden font-medium text-gray-500">
نام زبان
</span>
{post?.translations[0]?.lang?.title}
</td>
<td className="px-4 py-6 block md:table-cell text-center border-b border-b-neutral-200">
<span className="md:hidden font-medium text-gray-500">
اکشن ها
</span>
<div className="flex items-center justify-center gap-x-4">
<RoleGuard roles={["DEVELOPER"]}>
<DeleteExpertiseButton id={post.id} route="expertise" />
</RoleGuard>
<RoleGuard roles={["ADMIN", "DEVELOPER", "COORDINATOR"]}>
<Link
className="bg-primary text-white p-1.5 rounded-md"
href={`/expertise/edit/${post.id}`}
>
<Pencil />
</Link>
</RoleGuard>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{total > 0 && (
<Pagination dir="ltr">
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={() => setPage((p) => Math.max(1, p - 1))}
className={
page === 1
? "pointer-events-none opacity-50"
: "cursor-pointer"
}
/>
</PaginationItem>
{Array.from({length: totalPages}).map((_, i) => {
const pageNumber = i + 1;
return (
<PaginationItem key={pageNumber}>
<PaginationLink
isActive={page === pageNumber}
onClick={() => setPage(pageNumber)}
className="cursor-pointer"
>
{pageNumber}
</PaginationLink>
</PaginationItem>
);
})}
<PaginationItem>
<PaginationNext
title="بعدی"
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
className={
page === totalPages
? "pointer-events-none opacity-50"
: "cursor-pointer"
}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)}
</>
);
}

View File

@@ -0,0 +1,30 @@
import * as Icons from "lucide-react";
interface FeatureIconProps extends Icons.LucideProps {
iconName?: string;
label?: string;
hasLabel:boolean
}
export const FeatureIcon: React.FC<FeatureIconProps> = ({
iconName,
label,
hasLabel=true,
...props
}) => {
const LucideIcon =
iconName && iconName in Icons
? (Icons[
iconName as keyof typeof Icons
] as React.ComponentType<Icons.LucideProps>)
: null;
return (
<div className="flex flex-col items-center justify-center gap-1 text-sm text-center">
{LucideIcon && <LucideIcon size={18} {...props} />}
{hasLabel && <span>{label}</span>}
</div>
);
};
export default FeatureIcon;

View File

@@ -0,0 +1,45 @@
"use client";
import React from "react";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "./ui/select";
import {useRouter, useSearchParams} from "next/navigation";
export default function FilterByisDeleted() {
const urlSearchParams = useSearchParams();
const searchParams = new URLSearchParams(urlSearchParams);
const router = useRouter();
const handleFilter = (value: string) => {
if (value === "true") {
searchParams.set("is_deleted", value);
}else{
searchParams.delete("is_deleted");
}
router.push(
`/patients${searchParams ? `?${searchParams.toString()}` : ""}`
);
};
return (
<>
<Select onValueChange={handleFilter}>
<SelectTrigger className="w-[180px] bg-white" dir="rtl">
<SelectValue placeholder=" انتخاب کنید" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="false">همه</SelectItem>
<SelectItem value={"true"}>حذف شده ها</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</>
);
}

View File

@@ -0,0 +1,165 @@
"use client";
import * as React from "react";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import Loader from "./Loader";
import {useRouter} from "next/navigation";
import RoleGuard from "./RoleGuard";
import DeleteLanguageButton from "./DeleteLanguageButton";
import {useGetLanguages, useUpdateLanguage} from "@/hooks/languages";
import {Dialog} from "./ui/dialog";
import UpdateLanguageForm from "./forms/languages/update/UpdateLanguageForm";
import { useQueryClient } from "@tanstack/react-query";
export type ApiResponse<T> = {
status: number;
message: string;
data: T[];
};
export type Data = {
id: number;
title: string;
slug: string;
};
export default function LanguagesTable() {
const [page, setPage] = React.useState(1);
const [pageSize] = React.useState(20);
const {data, isLoading} = useGetLanguages();
const [total] = React.useState(0);
const {mutateAsync, isPending} = useUpdateLanguage();
const totalPages = Math.ceil(total / pageSize);
const queryClient = useQueryClient();
const router = useRouter();
const languages = data ? (data?.data as Data[]) : [];
if (isLoading) {
return (
<div className="text-sm text-muted-foreground">
<Loader />
</div>
);
}
return (
<>
<div className="grid gap-4"></div>
<div className="overflow-x-auto " dir="rtl">
<table className="min-w-full border border-gray-200 rounded-lg bg-white">
<thead className="bg-gray-100 hidden md:table-header-group">
<tr>
<th className="px-4 py-2 text-center text-sm font-semibold">
عنوان
</th>
<th className="px-4 py-2 text-center text-sm font-semibold">
اسلاگ
</th>
<th className="px-4 py-2 text-center text-sm font-semibold">
اکشن ها
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-400">
{languages?.map((post) => (
<tr
key={post.id}
className="block md:table-row border-b md:border-none"
>
<td className="px-4 py-6 block md:table-cell text-center text-sm border-b border-b-neutral-200">
<span className="md:hidden font-medium text-gray-500">
عنوان
</span>
{post?.title}
</td>
<td className="px-4 py-6 block md:table-cell text-center text-sm border-b border-b-neutral-200">
<span className="md:hidden font-medium text-gray-500">
اسلاگ
</span>
{post?.slug}
</td>
<td className="px-4 py-6 block md:table-cell text-center border-b border-b-neutral-200">
<span className="md:hidden font-medium text-gray-500">
اکشن ها
</span>
<div className="flex items-center justify-center gap-x-4">
<RoleGuard roles={["DEVELOPER"]}>
<DeleteLanguageButton id={post.id} route="language" />
</RoleGuard>
<RoleGuard roles={["ADMIN", "DEVELOPER", "COORDINATOR"]}>
<Dialog>
<UpdateLanguageForm
router={router}
updateFn={mutateAsync}
updatePending={isPending}
id={post.id ? String(post.id) : ""}
queryClient={queryClient}
preValues={post}
/>
</Dialog>
</RoleGuard>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{total > 0 && (
<Pagination dir="ltr">
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={() => setPage((p) => Math.max(1, p - 1))}
className={
page === 1
? "pointer-events-none opacity-50"
: "cursor-pointer"
}
/>
</PaginationItem>
{Array.from({length: totalPages}).map((_, i) => {
const pageNumber = i + 1;
return (
<PaginationItem key={pageNumber}>
<PaginationLink
isActive={page === pageNumber}
onClick={() => setPage(pageNumber)}
className="cursor-pointer"
>
{pageNumber}
</PaginationLink>
</PaginationItem>
);
})}
<PaginationItem>
<PaginationNext
title="بعدی"
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
className={
page === totalPages
? "pointer-events-none opacity-50"
: "cursor-pointer"
}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)}
</>
);
}

View File

@@ -0,0 +1,91 @@
"use client";
import React, {SetStateAction} from "react";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "./ui/select";
import {useRouter, useSearchParams} from "next/navigation";
import {API_URL} from "@/constants";
import Loader from "./Loader";
interface Data {
id: number;
slug: string;
title:string
}
async function fetchData() {
const res = await fetch(`${API_URL}/language/get/all`, {
cache: "no-store",
credentials: "include",
});
if (!res.ok) {
throw new Error("Failed to fetch data");
}
const data = await res.json();
return data;
}
export default function LanguagesFilterBox({
lang,
setLang,
}: {
lang: string;
setLang: React.Dispatch<SetStateAction<string>>;
}) {
const urlSearchParams = useSearchParams();
const [data, setData] = React.useState([]);
const [loading, setLoading] = React.useState(false);
const searchParams = new URLSearchParams(urlSearchParams);
const router = useRouter();
React.useEffect(() => {
let active = true;
setLoading(true);
fetchData()
.then((res) => {
if (!active) return;
console.log(res.data)
setData(res.data);
})
.catch(console.error)
.finally(() => active && setLoading(false));
return () => {
active = false;
};
}, []);
if (loading) {
return (
<div className="text-sm text-muted-foreground">
<Loader />
</div>
);
}
return (
<>
<Select onValueChange={(value) => setLang(value)}>
<SelectTrigger className="w-[180px] bg-white" dir="rtl">
<SelectValue placeholder="زبان انتخاب کنید" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{data?.map((item: Data) => (
<SelectItem key={item.id} value={item.slug}>
{item.title}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</>
);
}

Some files were not shown because too many files have changed in this diff Show More