Merge branch 'feature/amiriis_teleport_setad' into 'develop'

Feature/amiriis teleport setad

See merge request witel-front-end/crm-app!49
This commit is contained in:
AmirHossein Mahmoodi
2025-08-16 06:25:40 +00:00
20 changed files with 217 additions and 126 deletions

1
.gitignore vendored
View File

@@ -39,3 +39,4 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
/.idea

8
.idea/.gitignore generated vendored
View File

@@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@@ -1,47 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="LINE_SEPARATOR" value="&#13;&#10;" />
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</TypeScriptCodeStyleSettings>
<VueCodeStyleSettings>
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
</VueCodeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="120" />
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="SOFT_MARGINS" value="120" />
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

View File

@@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

12
.idea/crm-app.iml generated
View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -1,12 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="JSAnnotator" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
</profile>
</component>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/crm-app.iml" filepath="$PROJECT_DIR$/.idea/crm-app.iml" />
</modules>
</component>
</project>

6
.idea/prettier.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myConfigurationMode" value="AUTOMATIC" />
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -0,0 +1,10 @@
import TeleportPage from "@/components/Teleport";
export const metadata = {
title: "درحال انتقال به استان مورد نظر",
};
const Page = () => {
return <TeleportPage />;
};
export default Page;

View File

@@ -46,7 +46,7 @@ const LoginForm = () => {
data: formData,
});
setToken(response.data.data.token);
} catch (error) {}
} catch (error) { }
};
return (

View File

@@ -0,0 +1,51 @@
'use client'
import LoadingHardPage from "@/core/components/LoadingHardPage";
import { TELEPORT_USER_ROUTE } from "@/core/utils/routes";
import { useAuth } from "@/lib/contexts/auth";
import useRequest from "@/lib/hooks/useRequest";
import { Stack, Typography } from "@mui/material";
import { useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
const TelePortPage = () => {
const { setToken } = useAuth()
const searchParams = useSearchParams();
const token = searchParams.get("token");
const user_id = searchParams.get("user_id");
const [error, setError] = useState(false)
const requestServer = useRequest({ notificationShow: false });
useEffect(() => {
const login = async () => {
try {
const response = await requestServer(TELEPORT_USER_ROUTE, "post", {
data: {
token: decodeURIComponent(token),
user_id: decodeURIComponent(user_id)
}
});
setToken(response.data.data.token);
} catch (error) { setError(true) }
}
login()
}, [])
return (
<LoadingHardPage
authState={error}
label={
error ? (
<Stack justifyContent={"center"} alignItems={"center"} spacing={2}>
<Typography variant={"body1"}>مشکلی در انتقال پیش آمده است!</Typography>
</Stack>
) : (
<Typography variant={"body1"}>درحال انتقال به مرکز مورد نظر...</Typography>
)
}
loading={true}
/>
)
}
export default TelePortPage

View File

@@ -34,6 +34,18 @@ const DataTable = () => {
grow: false,
size: 100,
},
{
accessorKey: "name",
header: "نام انگلیسی",
id: "name",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "created_at",
header: "تاریخ ساخت",

View File

@@ -39,6 +39,7 @@ const DataTable = () => {
size: 100,
},
{
accessorKey: "telephone_id",
header: "شماره تماس داخلی",
id: "telephone_id",
enableColumnFilter: true,
@@ -48,7 +49,6 @@ const DataTable = () => {
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) => <TelephoneId user_id={row.original.id} />,
},
{
accessorKey: "is_online",

View File

@@ -5,7 +5,7 @@ import OnlineUsersReport from "./OnlineUsersReport";
const DashboardPage = () => {
return (
<Stack>
<OnlineUsersReport />
{/* <OnlineUsersReport /> */}
</Stack>
);
};

View File

@@ -1,12 +1,12 @@
"use client";
import Profile from "@/core/components/Profile";
import { filterMenuItems } from "@/core/utils/filterMenuItems";
import { pageMenu } from "@/core/utils/pageMenu";
import { usePermissions } from "@/lib/hooks/usePermissions";
import { List } from "@mui/material";
import { usePathname } from "next/navigation";
import { useEffect, useReducer, useState } from "react";
import { filterMenuItems } from "@/core/utils/filterMenuItems";
import { pageMenu } from "@/core/utils/pageMenu";
import SidebarListItems from "./SidebarListItems";
import Profile from "@/core/components/Profile";
function selectPage(item, route) {
if (item.type === "page") {
@@ -53,16 +53,16 @@ function reducer(state, action) {
return state.map((item) =>
item.hasSubitems
? {
...item,
Subitems: item.Subitems.map((subitem) =>
subitem.hasSubitems
? {
...subitem,
Subitems: toggleSubitems(subitem.Subitems, action.id),
}
: subitem
),
}
...item,
Subitems: item.Subitems.map((subitem) =>
subitem.hasSubitems
? {
...subitem,
Subitems: toggleSubitems(subitem.Subitems, action.id),
}
: subitem
),
}
: item
);

View File

@@ -8,8 +8,11 @@ import HeaderMenu from "./HeaderMenu";
import moment from "jalali-moment";
import { headerMenu } from "@/core/utils/headerMenu";
import SidebarMenu from "./Sidebar/SidebarMenu";
import Teleporting from "@/core/components/Teleporting";
import { usePermissions } from "@/lib/hooks/usePermissions";
import WithWidgetMiddleware from "@/core/middlewares/withWidget";
const drawerWidth = 300;
const drawerWidth = 270;
const Main = styled(Stack, { shouldForwardProp: (prop) => prop !== "open" })(({ theme, open }) => ({
flexGrow: 1,
@@ -50,11 +53,13 @@ const DrawerHeader = styled("div")(({ theme }) => ({
alignItems: "center",
padding: theme.spacing(0, 1),
...theme.mixins.toolbar,
minHeight: "40px !important",
minHeight: "43px !important",
justifyContent: "flex-end",
}));
const HeaderWithSidebar = ({ children }) => {
const { data: permissions } = usePermissions();
const isTeleportToProvince = permissions?.some((permission) => permission === "teleport_to_province");
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("md")); // Detect mobile/tablet screen
const [open, setOpen] = useState(!isMobile); // Initial state based on screen size
@@ -103,9 +108,16 @@ const HeaderWithSidebar = ({ children }) => {
},
}}
>
{headerMenu.map((menu) => (
<HeaderMenu key={menu.title} menu={menu} />
))}
<Stack sx={{ flex: 1 }} direction={'row'} justifyContent={'space-between'}>
<Box sx={{ flex: 1 }}>
{headerMenu.map((menu) => (
<HeaderMenu key={menu.title} menu={menu} />
))}
</Box>
<WithWidgetMiddleware enable={isTeleportToProvince}>
<Teleporting />
</WithWidgetMiddleware>
</Stack>
</Box>
</Toolbar>
</AppBar>

View File

@@ -0,0 +1,102 @@
import { GET_TOKEN_TELEPORT_USER_ROUTE } from "@/core/utils/routes";
import { useAuth } from "@/lib/contexts/auth";
import useProvinces from "@/lib/hooks/useProvince";
import useRequest from "@/lib/hooks/useRequest";
import { Apartment } from "@mui/icons-material";
import { Box, CircularProgress, FormControl, InputLabel, MenuItem, Select, Stack, Typography, Zoom } from "@mui/material";
import { useMemo, useState } from "react";
const Teleporting = () => {
const { user } = useAuth()
const { provinces, errorProvinces, loadingProvinces } = useProvinces()
const getColumnSelectOptions = useMemo(() => {
if (loadingProvinces) {
return [{ value: "loading", label: "در حال بارگذاری..." }];
}
if (errorProvinces) {
return [{ value: "error", label: "خطا در بارگذاری" }];
}
return [
...provinces.map((province) => ({
value: province.id,
label: province.name,
})),
];
}, [provinces, errorProvinces, loadingProvinces]);
const requestServer = useRequest();
const [loading, setLoading] = useState(false);
const [province, setProvince] = useState(user.server_province_id);
const handleChange = async ({ target: { value } }) => {
setLoading(true);
setProvince(value)
try {
const { data } = await requestServer(GET_TOKEN_TELEPORT_USER_ROUTE, "post", {
data: {
province_id: value
},
});
const url = `http://${data.data.ip}/teleport`
const params = `?token=${encodeURIComponent(data.data.token)}&user_id=${encodeURIComponent(data.data.user_id)}`;
window.location.href = url + params
} catch (error) {
setLoading(false);
setProvince(user.server_province_id)
}
};
if (loading) {
return (
<Zoom in>
<Stack
sx={{ width: 170, height: 43 }}
justifyContent="center"
alignItems="center"
direction="row"
spacing={1}
>
<CircularProgress size="14px" color="inherit" />
<Typography variant="caption">در حال انتقال به {provinces.find(p => p.id == province).name}</Typography>
</Stack>
</Zoom>
);
}
return (
<Stack sx={{ width: 170, height: 43 }}>
<FormControl
variant="filled"
size="small"
sx={{
minWidth: 170,
"& .MuiInputBase-root, & .MuiInputLabel-root, & .MuiSvgIcon-root": {
color: "white",
fontSize: "12px",
},
}}
>
<InputLabel id="teleport-province-label">مرکز</InputLabel>
<Select
labelId="teleport-province-label"
id="teleport-province"
value={loadingProvinces ? "loading" : errorProvinces ? "error" : province}
onChange={handleChange}
>
{getColumnSelectOptions.map(option => (
<MenuItem dense key={option.value} value={option.value}>
<Stack direction={'row'} alignItems={'center'} spacing={2}>
<Apartment />
<Box>
{option.label}
</Box>
</Stack>
</MenuItem>
))}
</Select>
</FormControl>
</Stack>
);
};
export default Teleporting;

View File

@@ -1,5 +1,8 @@
const api = process.env.NEXT_PUBLIC_API_URL;
export const TELEPORT_USER_ROUTE = api + "/verify_authentication";
export const GET_TOKEN_TELEPORT_USER_ROUTE = api + "/login_to_province";
export const GET_USER_ROUTE = api + "/profile/info";
export const CHANGE_USER_PASSWORD = api + "/profile/change_password";
export const GET_USER_LOGIN_ROUTE = api + "/auth/login";

View File

@@ -36,6 +36,7 @@ const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [errorState, setErrorState] = useState({ status: null, message: "" });
const [state, dispatch] = useReducer(authReducer, initAuth);
const [tokenChecked, setTokenChecked] = useState(false);
const clearUser = useCallback(() => {
dispatch({ type: "CLEAR_USER" });
@@ -95,9 +96,12 @@ export const AuthProvider = ({ children }) => {
useEffect(() => {
const localToken = localStorage.getItem("_token");
if (localToken) dispatch({ type: "SET_TOKEN", token: localToken });
setTokenChecked(true);
}, []);
useEffect(() => {
if (!tokenChecked) return;
if (!state.token) {
clearUser();
changeAuthState(false);
@@ -105,7 +109,7 @@ export const AuthProvider = ({ children }) => {
return;
}
getUser();
}, [state.token]);
}, [tokenChecked, state.token]);
return (
<AuthContext.Provider