first commit
This commit is contained in:
22
components.json
Normal file
22
components.json
Normal 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
1
debug.log
Normal 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)
|
||||
@@ -1,6 +1,16 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'http',
|
||||
hostname: 'localhost',
|
||||
port: '4000',
|
||||
pathname: '/uploads/**',
|
||||
},
|
||||
],
|
||||
},
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
|
||||
2740
package-lock.json
generated
2740
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
39
package.json
39
package.json
@@ -9,19 +9,48 @@
|
||||
"lint": "eslint"
|
||||
},
|
||||
"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-day-picker": "^9.13.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": {
|
||||
"typescript": "^5",
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"tailwindcss": "^4",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.5.7",
|
||||
"@eslint/eslintrc": "^3"
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
public/fonts/sogand/SOGAND.ttf
Normal file
BIN
public/fonts/sogand/SOGAND.ttf
Normal file
Binary file not shown.
BIN
public/fonts/vazir/Vazirmatn-Black.woff2
Normal file
BIN
public/fonts/vazir/Vazirmatn-Black.woff2
Normal file
Binary file not shown.
BIN
public/fonts/vazir/Vazirmatn-Bold.woff2
Normal file
BIN
public/fonts/vazir/Vazirmatn-Bold.woff2
Normal file
Binary file not shown.
BIN
public/fonts/vazir/Vazirmatn-ExtraBold.woff2
Normal file
BIN
public/fonts/vazir/Vazirmatn-ExtraBold.woff2
Normal file
Binary file not shown.
BIN
public/fonts/vazir/Vazirmatn-ExtraLight.woff2
Normal file
BIN
public/fonts/vazir/Vazirmatn-ExtraLight.woff2
Normal file
Binary file not shown.
BIN
public/fonts/vazir/Vazirmatn-Light.woff2
Normal file
BIN
public/fonts/vazir/Vazirmatn-Light.woff2
Normal file
Binary file not shown.
BIN
public/fonts/vazir/Vazirmatn-Medium.woff2
Normal file
BIN
public/fonts/vazir/Vazirmatn-Medium.woff2
Normal file
Binary file not shown.
BIN
public/fonts/vazir/Vazirmatn-Regular.woff2
Normal file
BIN
public/fonts/vazir/Vazirmatn-Regular.woff2
Normal file
Binary file not shown.
BIN
public/fonts/vazir/Vazirmatn-SemiBold.woff2
Normal file
BIN
public/fonts/vazir/Vazirmatn-SemiBold.woff2
Normal file
Binary file not shown.
BIN
public/fonts/vazir/Vazirmatn-Thin.woff2
Normal file
BIN
public/fonts/vazir/Vazirmatn-Thin.woff2
Normal file
Binary file not shown.
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 287 KiB |
13
src/app/(auth)/layout.tsx
Normal file
13
src/app/(auth)/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
24
src/app/(auth)/login/page.tsx
Normal file
24
src/app/(auth)/login/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
9
src/app/(auth)/reset-password/[token]/page.tsx
Normal file
9
src/app/(auth)/reset-password/[token]/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
18
src/app/(auth)/reset-password/page.tsx
Normal file
18
src/app/(auth)/reset-password/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
7
src/app/(dashboard)/configs/loading.tsx
Normal file
7
src/app/(dashboard)/configs/loading.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
26
src/app/(dashboard)/configs/page.tsx
Normal file
26
src/app/(dashboard)/configs/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
7
src/app/(dashboard)/dashboard/loading.tsx
Normal file
7
src/app/(dashboard)/dashboard/loading.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
25
src/app/(dashboard)/dashboard/page.tsx
Normal file
25
src/app/(dashboard)/dashboard/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
7
src/app/(dashboard)/default/loading.tsx
Normal file
7
src/app/(dashboard)/default/loading.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
33
src/app/(dashboard)/default/page.tsx
Normal file
33
src/app/(dashboard)/default/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
7
src/app/(dashboard)/department/loading.tsx
Normal file
7
src/app/(dashboard)/department/loading.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
64
src/app/(dashboard)/department/members/edit/[id]/page.tsx
Normal file
64
src/app/(dashboard)/department/members/edit/[id]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
7
src/app/(dashboard)/department/members/edit/page.tsx
Normal file
7
src/app/(dashboard)/department/members/edit/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
"use client"
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
export default function Page() {
|
||||
return notFound
|
||||
}
|
||||
103
src/app/(dashboard)/department/members/new/page.tsx
Normal file
103
src/app/(dashboard)/department/members/new/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
49
src/app/(dashboard)/department/members/page.tsx
Normal file
49
src/app/(dashboard)/department/members/page.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
64
src/app/(dashboard)/doctors/edit/[id]/page.tsx
Normal file
64
src/app/(dashboard)/doctors/edit/[id]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
10
src/app/(dashboard)/doctors/edit/page.tsx
Normal file
10
src/app/(dashboard)/doctors/edit/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
"use client"
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
|
||||
|
||||
|
||||
export default function Page() {
|
||||
|
||||
return notFound();
|
||||
}
|
||||
7
src/app/(dashboard)/doctors/loading.tsx
Normal file
7
src/app/(dashboard)/doctors/loading.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
103
src/app/(dashboard)/doctors/new/page.tsx
Normal file
103
src/app/(dashboard)/doctors/new/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
51
src/app/(dashboard)/doctors/page.tsx
Normal file
51
src/app/(dashboard)/doctors/page.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
62
src/app/(dashboard)/expertise/edit/[id]/page.tsx
Normal file
62
src/app/(dashboard)/expertise/edit/[id]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
7
src/app/(dashboard)/expertise/edit/loading.tsx
Normal file
7
src/app/(dashboard)/expertise/edit/loading.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
9
src/app/(dashboard)/expertise/edit/page.tsx
Normal file
9
src/app/(dashboard)/expertise/edit/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
7
src/app/(dashboard)/expertise/loading.tsx
Normal file
7
src/app/(dashboard)/expertise/loading.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
7
src/app/(dashboard)/expertise/new/loading.tsx
Normal file
7
src/app/(dashboard)/expertise/new/loading.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
40
src/app/(dashboard)/expertise/new/page.tsx
Normal file
40
src/app/(dashboard)/expertise/new/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
52
src/app/(dashboard)/expertise/page.tsx
Normal file
52
src/app/(dashboard)/expertise/page.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
7
src/app/(dashboard)/languages/loading.tsx
Normal file
7
src/app/(dashboard)/languages/loading.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
52
src/app/(dashboard)/languages/page.tsx
Normal file
52
src/app/(dashboard)/languages/page.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
53
src/app/(dashboard)/layout.tsx
Normal file
53
src/app/(dashboard)/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
7
src/app/(dashboard)/logs/access/loading.tsx
Normal file
7
src/app/(dashboard)/logs/access/loading.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
7
src/app/(dashboard)/logs/audit/loading.tsx
Normal file
7
src/app/(dashboard)/logs/audit/loading.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
7
src/app/(dashboard)/logs/errors/loading.tsx
Normal file
7
src/app/(dashboard)/logs/errors/loading.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
7
src/app/(dashboard)/logs/loading.tsx
Normal file
7
src/app/(dashboard)/logs/loading.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
7
src/app/(dashboard)/logs/performance/loading.tsx
Normal file
7
src/app/(dashboard)/logs/performance/loading.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
7
src/app/(dashboard)/logs/policy/loading.tsx
Normal file
7
src/app/(dashboard)/logs/policy/loading.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
7
src/app/(dashboard)/logs/requests/loading.tsx
Normal file
7
src/app/(dashboard)/logs/requests/loading.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
7
src/app/(dashboard)/logs/upload-server/loading.tsx
Normal file
7
src/app/(dashboard)/logs/upload-server/loading.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
43
src/app/(dashboard)/logs/upload-server/page.tsx
Normal file
43
src/app/(dashboard)/logs/upload-server/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
73
src/app/(dashboard)/medical-packages/edit/[id]/page.tsx
Normal file
73
src/app/(dashboard)/medical-packages/edit/[id]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
9
src/app/(dashboard)/medical-packages/edit/page.tsx
Normal file
9
src/app/(dashboard)/medical-packages/edit/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
30
src/app/(dashboard)/medical-packages/new/page.tsx
Normal file
30
src/app/(dashboard)/medical-packages/new/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
49
src/app/(dashboard)/medical-packages/page.tsx
Normal file
49
src/app/(dashboard)/medical-packages/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
15
src/app/(dashboard)/online-case/new/page.tsx
Normal file
15
src/app/(dashboard)/online-case/new/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
46
src/app/(dashboard)/online-case/page.tsx
Normal file
46
src/app/(dashboard)/online-case/page.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
66
src/app/(dashboard)/patients/edit/[id]/page.tsx
Normal file
66
src/app/(dashboard)/patients/edit/[id]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
7
src/app/(dashboard)/patients/loading.tsx
Normal file
7
src/app/(dashboard)/patients/loading.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
32
src/app/(dashboard)/patients/new/page.tsx
Normal file
32
src/app/(dashboard)/patients/new/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
108
src/app/(dashboard)/patients/page.tsx
Normal file
108
src/app/(dashboard)/patients/page.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
10
src/app/(dashboard)/patients/restore/page.tsx
Normal file
10
src/app/(dashboard)/patients/restore/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import RestorePatientTable from '@/components/RestorePatientTable'
|
||||
import React from 'react'
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
<RestorePatientTable />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
60
src/app/(dashboard)/privacy-policy/page.tsx
Normal file
60
src/app/(dashboard)/privacy-policy/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
193
src/app/(dashboard)/tos/page.tsx
Normal file
193
src/app/(dashboard)/tos/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
28
src/app/(dashboard)/transfer-packages/new/page.tsx
Normal file
28
src/app/(dashboard)/transfer-packages/new/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
9
src/app/(dashboard)/transfer-packages/page.tsx
Normal file
9
src/app/(dashboard)/transfer-packages/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
7
src/app/(dashboard)/transfer-team/loading.tsx
Normal file
7
src/app/(dashboard)/transfer-team/loading.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
67
src/app/(dashboard)/transfer-team/members/edit/[id]/page.tsx
Normal file
67
src/app/(dashboard)/transfer-team/members/edit/[id]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
6
src/app/(dashboard)/transfer-team/members/edit/page.tsx
Normal file
6
src/app/(dashboard)/transfer-team/members/edit/page.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
"use client";
|
||||
import {notFound} from "next/navigation";
|
||||
|
||||
export default function Page() {
|
||||
return notFound();
|
||||
}
|
||||
105
src/app/(dashboard)/transfer-team/members/new/page.tsx
Normal file
105
src/app/(dashboard)/transfer-team/members/new/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
49
src/app/(dashboard)/transfer-team/members/page.tsx
Normal file
49
src/app/(dashboard)/transfer-team/members/page.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
41
src/app/(dashboard)/transfer-team/teams/new/page.tsx
Normal file
41
src/app/(dashboard)/transfer-team/teams/new/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
38
src/app/(dashboard)/transfer-team/teams/page.tsx
Normal file
38
src/app/(dashboard)/transfer-team/teams/page.tsx
Normal 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
17
src/app/error.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +1,147 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--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 {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: #f6f6f6;
|
||||
--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 {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-family: VazirMatn, Arial, Helvetica, sans-serif;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import type {Metadata} from "next";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
import {FontVazir} from "@/config/font.config";
|
||||
import {ToastContainer} from "react-toastify";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
title: "پنل مدیریت وب سایت بیماران بین الملل",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
@@ -23,11 +14,22 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" dir="rtl">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
className={`${FontVazir.variable} antialiased`}
|
||||
style={{fontFamily: FontVazir.style.fontFamily}}
|
||||
>
|
||||
{children}
|
||||
<ToastContainer
|
||||
style={{fontFamily: FontVazir.style.fontFamily,fontSize:'14px'}}
|
||||
position="top-right"
|
||||
autoClose={3000}
|
||||
hideProgressBar={false}
|
||||
newestOnTop
|
||||
closeOnClick
|
||||
pauseOnHover
|
||||
draggable
|
||||
/>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
16
src/app/not-found.tsx
Normal file
16
src/app/not-found.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
102
src/app/page.tsx
102
src/app/page.tsx
@@ -1,103 +1,47 @@
|
||||
import { LogIn } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Home() {
|
||||
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">
|
||||
<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
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
src="/logo.png"
|
||||
alt="Shomal Amol Hospital"
|
||||
width={280}
|
||||
height={48}
|
||||
priority
|
||||
/>
|
||||
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
|
||||
<li className="mb-2 tracking-[-.01em]">
|
||||
Get started by editing{" "}
|
||||
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
|
||||
src/app/page.tsx
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li className="tracking-[-.01em]">
|
||||
Save and see your changes instantly.
|
||||
</li>
|
||||
</ol>
|
||||
<h4 className="mb-1 tracking-[-.01em] font-bold text-xl">
|
||||
پنل مدیریت سایت بیماران بین الملل
|
||||
</h4>
|
||||
{/* <p className="tracking-[-.01em]">
|
||||
یکی از دکمه های زیر را فشار دهید
|
||||
</p> */}
|
||||
|
||||
<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"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
href="/dashboard"
|
||||
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Deploy now
|
||||
</a>
|
||||
<LogIn />
|
||||
ورود به پنل
|
||||
</Link>
|
||||
<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]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
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://ipd.shomalhospital.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read our docs
|
||||
مشاهده وب سایت
|
||||
</a>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
27
src/components/AuthGuard.tsx
Normal file
27
src/components/AuthGuard.tsx
Normal 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;
|
||||
}
|
||||
49
src/components/CalendarHijri.tsx
Normal file
49
src/components/CalendarHijri.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
49
src/components/CalendarNormal.tsx
Normal file
49
src/components/CalendarNormal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
184
src/components/DashboardCDNServerStatus.tsx
Normal file
184
src/components/DashboardCDNServerStatus.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
136
src/components/DashboardStatistics.tsx
Normal file
136
src/components/DashboardStatistics.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
67
src/components/DeleteExpertiseButton.tsx
Normal file
67
src/components/DeleteExpertiseButton.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
40
src/components/DeleteFileButton.tsx
Normal file
40
src/components/DeleteFileButton.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
69
src/components/DeleteLanguageButton.tsx
Normal file
69
src/components/DeleteLanguageButton.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
69
src/components/DeleteMedicalPackageButton.tsx
Normal file
69
src/components/DeleteMedicalPackageButton.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
8
src/components/DenyAccess.tsx
Normal file
8
src/components/DenyAccess.tsx
Normal 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} ایجاد کنید. برای ایجاد روی <Link href={link} className="underline underline-offset-4"> لینک </Link> کلیک کنید</div>
|
||||
)
|
||||
}
|
||||
260
src/components/DepartmentMembersTable.tsx
Normal file
260
src/components/DepartmentMembersTable.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
261
src/components/DoctorsTable.tsx
Normal file
261
src/components/DoctorsTable.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
94
src/components/ExpertiseFilterBox.tsx
Normal file
94
src/components/ExpertiseFilterBox.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
234
src/components/ExpertiseTable.tsx
Normal file
234
src/components/ExpertiseTable.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
30
src/components/FeatureIcon.tsx
Normal file
30
src/components/FeatureIcon.tsx
Normal 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;
|
||||
45
src/components/FilterByisDeleted.tsx
Normal file
45
src/components/FilterByisDeleted.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
165
src/components/LangaugesTable.tsx
Normal file
165
src/components/LangaugesTable.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
91
src/components/LanguagesFilterBox.tsx
Normal file
91
src/components/LanguagesFilterBox.tsx
Normal 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
Reference in New Issue
Block a user