Merge branch 'feature/operator_rating_page' into 'develop'

Feature/operator rating page

See merge request witel-front-end/crm-app!48
This commit is contained in:
AmirHossein Mahmoodi
2025-08-16 05:46:57 +00:00
33 changed files with 1821 additions and 10 deletions

View File

@@ -3,5 +3,10 @@
<option name="myName" value="Project Default" /> <option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" /> <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="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> </profile>
</component> </component>

View File

@@ -0,0 +1,16 @@
import WithPermission from "@/core/middlewares/withPermission";
import OperatorDialoguePage from "@/components/dashboard/monitoring/operator/OperatorDialogue";
export const metadata = {
title: "پیام های سیستمی",
};
const Page = () => {
return (
<WithPermission permission_name={["all"]}>
<OperatorDialoguePage />
</WithPermission>
);
};
export default Page;

View File

@@ -0,0 +1,16 @@
import WithPermission from "@/core/middlewares/withPermission";
import OperatorDialoguePage from "@/components/dashboard/monitoring/supervisor/OperatorDialogue";
export const metadata = {
title: "ارزیابی پیام های سیستمی",
};
const Page = () => {
return (
<WithPermission permission_name={["all"]}>
<OperatorDialoguePage />
</WithPermission>
);
};
export default Page;

View File

@@ -0,0 +1,16 @@
import WithPermission from "@/core/middlewares/withPermission";
import PeopleMessagePage from "@/components/dashboard/monitoring/operator/PeopleMessage";
export const metadata = {
title: "پیام های سیستمی",
};
const Page = () => {
return (
<WithPermission permission_name={["all"]}>
<PeopleMessagePage />
</WithPermission>
);
};
export default Page;

View File

@@ -0,0 +1,16 @@
import WithPermission from "@/core/middlewares/withPermission";
import PeopleMessagePage from "@/components/dashboard/monitoring/supervisor/PeopleMessage";
export const metadata = {
title: "پیام های سیستمی",
};
const Page = () => {
return (
<WithPermission permission_name={["all"]}>
<PeopleMessagePage />
</WithPermission>
);
};
export default Page;

View File

@@ -1,5 +1,5 @@
import WithPermission from "@/core/middlewares/withPermission"; import WithPermission from "@/core/middlewares/withPermission";
import SystemMessagesPage from "@/components/dashboard/monitoring/SystemMessages"; import SystemMessagesPage from "@/components/dashboard/monitoring/operator/SystemMessage";
export const metadata = { export const metadata = {
title: "پیام های سیستمی", title: "پیام های سیستمی",

View File

@@ -0,0 +1,16 @@
import WithPermission from "@/core/middlewares/withPermission";
import SystemMessagesPage from "@/components/dashboard/monitoring/supervisor/SystemMessages";
export const metadata = {
title: "پیام های سیستمی",
};
const Page = () => {
return (
<WithPermission permission_name={["all"]}>
<SystemMessagesPage />
</WithPermission>
);
};
export default Page;

View File

@@ -0,0 +1,140 @@
"use client";
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_OPERATOR_DIALOGUE_OPERATOR } from "@/core/utils/routes";
import { Box } from "@mui/material";
import { useMemo } from "react";
import moment from "jalali-moment";
import Player from "./Player";
import CheckIcon from "@mui/icons-material/Check";
import CloseIcon from "@mui/icons-material/Close";
const DataTable = () => {
const columns = useMemo(
() => [
{
header: "تاریخ",
id: "date",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) =>
row.original.calldate ? moment(row.original.calldate).locale("fa").format("yyyy/MM/DD") : <>-</>,
},
{
header: "ساعت",
id: "time",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) =>
row.original.calldate ? moment(row.original.calldate).locale("fa").format("HH:mm") : <>-</>,
},
{
accessorKey: "src",
header: "شماره تماس گیرنده",
id: "src",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "dst",
header: "کارشناس",
id: "dst",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
header: "پخش صدا",
id: "is_listen",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) => {
const isListen = row.original.is_listen;
const Icon = isListen ? CheckIcon : CloseIcon;
const color = isListen ? "success.main" : "error.main";
return (
<Box sx={{ display: "flex", justifyContent: "center" }}>
<Icon sx={{ color }} />
</Box>
);
}
},
{
accessorKey: "disposition",
header: "وضعیت",
id: "disposition",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "duration",
header: "طول تماس ( ثانیه )",
id: "duration",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
header: "پخش صدا",
id: "player",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) => <Player audioUrl={row.original.recordingfile} messageId={row.original.id} />,
},
],
[]
);
return (
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
<DataTableWithAuth
need_filter={true}
columns={columns}
table_url={GET_OPERATOR_DIALOGUE_OPERATOR}
page_name={"operatorDialoguePage"}
table_name={"operatorDialogueList"}
positionActionsColumn={"last"}
/>
</Box>
);
};
export default DataTable;

View File

@@ -0,0 +1,153 @@
"use client";
import { Button, FormControl, FormHelperText, Grid, InputLabel, MenuItem, OutlinedInput, Select } from "@mui/material";
import LtrTextField from "@/core/components/LtrTextField";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { object, string, number } from "yup";
import { GET_SYSTEM_MESSAGES_SUPERVISOR } from "@/core/utils/routes";
import StyledForm from "@/core/components/StyledForm";
import useRequest from "@/lib/hooks/useRequest";
import usePermissionsList from "@/lib/hooks/usePermissionsList";
import { useEffect } from "react";
const RateMessage = ({ date, messageId, initialValues, mutate }) => {
const requestServer = useRequest({ notificationSuccess: true });
const defaultValues = {
supervisor_id: initialValues?.supervisor_id ?? "",
is_good: initialValues?.is_good ?? "",
description: initialValues?.description ?? "",
};
const validationSchema = object().shape({
supervisor_id: string().required("کارشناس بررسی کننده را مشخص کنید"),
is_good: string().required("نحوه پاسخگویی را مشخص کنید"),
description: string().optional(),
});
const {
control,
handleSubmit,
reset,
formState: { isSubmitting, errors, isDirty, isValid },
} = useForm({ defaultValues, resolver: yupResolver(validationSchema) });
const onSubmit = async (data) => {
const fixData = {
date: date,
cdr_unique_id: messageId,
supervisor_id: data.supervisor_id,
is_good: data.is_good,
...(data.description !== "" && { description: data.description }),
};
try {
await requestServer(`${GET_SYSTEM_MESSAGES_SUPERVISOR}`, "post", {
data: fixData,
});
mutate();
reset({
supervisor_id: initialValues?.supervisor_id ?? "",
is_good: initialValues?.is_good ?? "",
description: initialValues?.description ?? "",
});
} catch (error) {}
};
useEffect(() => {
reset({
supervisor_id: initialValues?.supervisor_id ?? "",
is_good: initialValues?.is_good ?? "",
description: initialValues?.description ?? "",
});
}, [initialValues.supervisor_id, initialValues.is_good, initialValues.description]);
return (
<StyledForm id="monitoringForm" onSubmit={handleSubmit(onSubmit)} style={{ width: "100%", height: "100%" }}>
<Grid container spacing={2} sx={{ flexWrap: "nowrap", alignItems: "start" }}>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
variant="outlined"
label="کارشناس بررسی کننده"
size="small"
sx={{
width: "180px",
}}
InputProps={{ sx: { fontSize: "12px" } }}
InputLabelProps={{ sx: { fontSize: "12px" } }}
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name="supervisor_id"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
variant="outlined"
label="نحوه پاسخگویی"
size="small"
sx={{
width: "180px",
}}
InputProps={{ sx: { fontSize: "12px" } }}
InputLabelProps={{ sx: { fontSize: "12px" } }}
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name="is_good"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
variant="outlined"
label="توضیحات"
size="small"
sx={{
width: "180px",
}}
InputProps={{ sx: { fontSize: "12px" } }}
InputLabelProps={{ sx: { fontSize: "12px" } }}
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name="description"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Button
type="submit"
disabled={isSubmitting || !isDirty || !isValid}
size="medium"
autoFocus
variant="contained"
>
{isSubmitting ? "درحال ثبت..." : "ثبت"}
</Button>
</Grid>
</Grid>
</StyledForm>
);
};
export default RateMessage;

View File

@@ -0,0 +1,16 @@
"use client";
import PageTitle from "@/core/components/PageTitle";
import { Stack } from "@mui/material";
import DataTable from "./DataTable";
const OperatorDialoguePage = () => {
return (
<Stack spacing={1}>
<PageTitle title={"صدای کارشناسان"} />
<DataTable />
</Stack>
);
};
export default OperatorDialoguePage;

View File

@@ -0,0 +1,167 @@
"use client";
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_OPERATOR_PEOPLE_MESSAGE_OPERATOR } from "@/core/utils/routes";
import { Box } from "@mui/material";
import { useMemo } from "react";
import moment from "jalali-moment";
import Player from "./Player";
import CloseIcon from '@mui/icons-material/Close';
import CheckIcon from '@mui/icons-material/Check';
import RateMessage from "./RateMessage";
const DataTable = () => {
const columns = useMemo(
() => [
{
accessorKey: "id",
header: "کد یکتا",
id: "id",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
header: "تاریخ",
id: "date",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) =>
row.original.date ? moment(row.original.date).locale("fa").format("yyyy/MM/DD") : <>-</>,
},
{
header: "ساعت",
id: "time",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) =>
row.original.date ? moment(row.original.date).locale("fa").format("HH:mm") : <>-</>,
},
{
accessorKey: "caller_phone_number",
header: "شماره تماس",
id: "caller_phone_number",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
header: "پخش صدا",
id: "player",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) => <Player audioUrl={row.original.voice} messageId={row.original.id} />,
},
{
header: "پخش صدا",
id: "is_listen",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) => {
const isListen = row.original.is_listen;
const Icon = isListen ? CheckIcon : CloseIcon;
const color = isListen ? "success.main" : "error.main";
return (
<Box sx={{ display: "flex", justifyContent: "center" }}>
<Icon sx={{ color }} />
</Box>
);
}
},
{
accessorKey: "listen_at",
header: "زمان شنیده شدن",
id: "listen_at",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "review_duration",
header: "زمان بررسی (دقیقه)",
id: "review_duration",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
header: "ارزیابی",
id: "rate",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ cell, row, table }) => {
const mutate = table.options.meta?.mutate;
return (
<RateMessage
mutate={mutate}
messageId={row.original.id}
initialValues={{
savaneh_id: row.original.savaneh_id,
action_id: row.original.action_id,
description: row.original.description,
}}
/>
);
},
},
],
[]
);
return (
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
<DataTableWithAuth
need_filter={true}
columns={columns}
table_url={GET_OPERATOR_PEOPLE_MESSAGE_OPERATOR}
page_name={"operatorDialoguePage"}
table_name={"operatorDialogueList"}
positionActionsColumn={"last"}
/>
</Box>
);
};
export default DataTable;

View File

@@ -0,0 +1,45 @@
"use client";
import React from "react";
import { IconButton, Box, Typography } from "@mui/material";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import PauseIcon from "@mui/icons-material/Pause";
import useWaveSurferPlayer from "@/lib/hooks/useWaveSurferPlayer";
function formatTime(seconds) {
if (!seconds || isNaN(seconds)) return "0:00";
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${minutes}:${secs.toString().padStart(2, "0")}`;
}
const Player = ({ audioUrl }) => {
const { containerRef, isPlaying, playPause, currentTime, duration } =
useWaveSurferPlayer(audioUrl);
return (
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 1,
justifyContent: "center",
}}
>
<div ref={containerRef} style={{ width: 180 }} />
<IconButton aria-label="play-pause" onClick={playPause}>
{isPlaying ? (
<PauseIcon sx={{ color: "#d32f2f" }} />
) : (
<PlayArrowIcon sx={{ color: "#1b4332" }} />
)}
</IconButton>
<Typography variant="body2" sx={{ minWidth: 60, textAlign: "right" }}>
{formatTime(currentTime)} / {formatTime(duration)}
</Typography>
</Box>
);
};
export default Player;

View File

@@ -0,0 +1,159 @@
"use client";
import { Button, FormControl, Grid, InputLabel, MenuItem, Select } from "@mui/material";
import LtrTextField from "@/core/components/LtrTextField";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { object, string } from "yup";
import { GET_OPERATOR_PEOPLE_MESSAGE_OPERATOR } from "@/core/utils/routes";
import StyledForm from "@/core/components/StyledForm";
import useRequest from "@/lib/hooks/useRequest";
import { useEffect } from "react";
const RateMessage = ({ messageId, initialValues, mutate }) => {
const requestServer = useRequest({ notificationSuccess: true });
const defaultValues = {
savaneh_id: initialValues?.savaneh_id ?? "",
action_id: initialValues?.action_id ?? "",
description: initialValues?.description ?? "",
};
const validationSchema = object().shape({
savaneh_id: string().required("وضعیت کیفیت پیام را مشخص کنید"),
action_id: string().optional(),
description: string().optional(),
});
const {
control,
handleSubmit,
reset,
formState: { isSubmitting, errors, isDirty, isValid },
} = useForm({ defaultValues, resolver: yupResolver(validationSchema) });
const onSubmit = async (data) => {
const fixData = {
savaneh_id: parseInt(data.savaneh_id),
...(data.action_id !== "" && { action_id: data.action_id }),
...(data.description !== "" && { description: data.description }),
};
try {
await requestServer(`${GET_OPERATOR_PEOPLE_MESSAGE_OPERATOR}/${messageId}`, "post", {
data: fixData,
});
mutate();
reset({
savaneh_id: initialValues?.savaneh_id ?? "",
action_id: initialValues?.action_id ?? "",
description: initialValues?.description ?? "",
});
} catch (error) {}
};
useEffect(() => {
reset({
savaneh_id: initialValues?.savaneh_id ?? "",
action_id: initialValues?.action_id ?? "",
description: initialValues?.description ?? "",
});
}, [initialValues.savaneh_id, initialValues.action_id, initialValues.description]);
return (
<StyledForm id="monitoringForm" onSubmit={handleSubmit(onSubmit)} style={{ width: "100%", height: "100%" }}>
<Grid container spacing={2} sx={{ flexWrap: "nowrap", alignItems: "start" }}>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<FormControl fullWidth error={!!error} variant="outlined">
<InputLabel id="label-message-quality" size="small" sx={{ fontSize: "12px" }}>
پیگیری
</InputLabel>
<Select
variant="outlined"
labelId="label-message-quality"
id="savaneh_id"
name="savaneh_id"
size="small"
sx={{ width: "100px", fontSize: "12px" }}
value={field.value}
label="پیگیری"
onChange={(e) => field.onChange(e.target.value)}
>
<MenuItem value={1}>عدم نیاز به ثبت (مشکل راه)</MenuItem>
<MenuItem value={2}>عدم نیاز به ثبت (مشکل حمل و نقلی)</MenuItem>
<MenuItem value={3}>عدم نیاز به ثبت (مشکل عوارضی)</MenuItem>
<MenuItem value={4}>ثبت سوانح (مشکل راه)</MenuItem>
<MenuItem value={5}>ثبت سوانح (مشکل حمل و نقلی)</MenuItem>
</Select>
</FormControl>
);
}}
name="savaneh_id"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
variant="outlined"
label="کد واقعه"
size="small"
sx={{
width: "180px",
}}
InputProps={{ sx: { fontSize: "12px" } }}
InputLabelProps={{ sx: { fontSize: "12px" } }}
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name="action_id"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
variant="outlined"
label="توضیحات"
size="small"
sx={{
width: "180px",
}}
InputProps={{ sx: { fontSize: "12px" } }}
InputLabelProps={{ sx: { fontSize: "12px" } }}
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name="description"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Button
type="submit"
disabled={isSubmitting || !isDirty || !isValid}
size="medium"
autoFocus
variant="contained"
>
{isSubmitting ? "درحال ثبت..." : "ثبت"}
</Button>
</Grid>
</Grid>
</StyledForm>
);
};
export default RateMessage;

View File

@@ -0,0 +1,16 @@
"use client";
import PageTitle from "@/core/components/PageTitle";
import { Stack } from "@mui/material";
import DataTable from "./DataTable";
const PeopleMessagePage = () => {
return (
<Stack spacing={1}>
<PageTitle title={"پیام های مردمی"} />
<DataTable />
</Stack>
);
};
export default PeopleMessagePage;

View File

@@ -0,0 +1,103 @@
"use client";
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_SYSTEM_MESSAGES_OPERATOR } from "@/core/utils/routes";
import { Box } from "@mui/material";
import { useMemo } from "react";
import moment from "jalali-moment";
import Player from "./Player";
const DataTable = () => {
const columns = useMemo(
() => [
{
accessorKey: "id",
header: "کد یکتا",
id: "id",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "type_name",
header: "نوع",
id: "type_name",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
header: "تاریخ",
id: "date",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) =>
row.original.date ? moment(row.original.date).locale("fa").format("yyyy/MM/DD") : <>-</>,
},
{
header: "ساعت",
id: "time",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) =>
row.original.date ? moment(row.original.date).locale("fa").format("HH:mm") : <>-</>,
},
{
accessorKey: "duration",
header: "طول تماس ( ثانیه )",
id: "duration",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
header: "پخش صدا",
id: "player",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) => <Player audioUrl={row.original.voice} messageId={row.original.id} />,
},
],
[]
);
return (
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
<DataTableWithAuth
need_filter={true}
columns={columns}
table_url={GET_SYSTEM_MESSAGES_OPERATOR}
page_name={"systemMessagesPage"}
table_name={"systemMessagesList"}
positionActionsColumn={"last"}
/>
</Box>
);
};
export default DataTable;

View File

@@ -0,0 +1,45 @@
"use client";
import React from "react";
import { IconButton, Box, Typography } from "@mui/material";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import PauseIcon from "@mui/icons-material/Pause";
import useWaveSurferPlayer from "@/lib/hooks/useWaveSurferPlayer";
function formatTime(seconds) {
if (!seconds || isNaN(seconds)) return "0:00";
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${minutes}:${secs.toString().padStart(2, "0")}`;
}
const Player = ({ audioUrl }) => {
const { containerRef, isPlaying, playPause, currentTime, duration } =
useWaveSurferPlayer(audioUrl);
return (
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 1,
justifyContent: "center",
}}
>
<div ref={containerRef} style={{ width: 180 }} />
<IconButton aria-label="play-pause" onClick={playPause}>
{isPlaying ? (
<PauseIcon sx={{ color: "#d32f2f" }} />
) : (
<PlayArrowIcon sx={{ color: "#1b4332" }} />
)}
</IconButton>
<Typography variant="body2" sx={{ minWidth: 60, textAlign: "right" }}>
{formatTime(currentTime)} / {formatTime(duration)}
</Typography>
</Box>
);
};
export default Player;

View File

@@ -0,0 +1,168 @@
"use client";
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_OPERATOR_DIALOGUE_SUPERVISOR } from "@/core/utils/routes";
import { Box } from "@mui/material";
import { useMemo } from "react";
import moment from "jalali-moment";
import Player from "./Player";
import RateMessage from "./RateMessage";
import CheckIcon from "@mui/icons-material/Check";
import CloseIcon from "@mui/icons-material/Close";
const DataTable = () => {
const columns = useMemo(
() => [
{
header: "تاریخ",
id: "date",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) =>
row.original.calldate ? moment(row.original.calldate).locale("fa").format("yyyy/MM/DD") : <>-</>,
},
{
header: "ساعت",
id: "time",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) =>
row.original.calldate ? moment(row.original.calldate).locale("fa").format("HH:mm") : <>-</>,
},
{
accessorKey: "src",
header: "شماره تماس گیرنده",
id: "src",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "dst",
header: "کارشناس",
id: "dst",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
header: "پخش صدا",
id: "is_listen",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) => {
const isListen = row.original.is_listen;
const Icon = isListen ? CheckIcon : CloseIcon;
const color = isListen ? "success.main" : "error.main";
return (
<Box sx={{ display: "flex", justifyContent: "center" }}>
<Icon sx={{ color }} />
</Box>
);
}
},
{
accessorKey: "disposition",
header: "وضعیت",
id: "disposition",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "duration",
header: "طول تماس ( ثانیه )",
id: "duration",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
header: "پخش صدا",
id: "player",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) => <Player audioUrl={row.original.recordingfile} messageId={row.original.id} />,
},
{
header: "ارزیابی",
id: "rate",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ cell, row, table }) => {
const mutate = table.options.meta?.mutate;
return (
<RateMessage
mutate={mutate}
date={row.original.calldate}
messageId={row.original.uniqueid}
initialValues={{
supervisor_id: row.original.supervisor_id,
is_good: row.original.is_good,
description: row.original.description,
}}
/>
);
},
},
],
[]
);
return (
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
<DataTableWithAuth
need_filter={true}
columns={columns}
table_url={GET_OPERATOR_DIALOGUE_SUPERVISOR}
page_name={"operatorDialoguePage"}
table_name={"operatorDialogueList"}
positionActionsColumn={"last"}
/>
</Box>
);
};
export default DataTable;

View File

@@ -0,0 +1,22 @@
"use client";
import React from "react";
import { IconButton, Box } from "@mui/material";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import PauseIcon from "@mui/icons-material/Pause";
import useWaveSurferPlayer from "@/lib/hooks/useWaveSurferPlayer";
const Player = ({ audioUrl }) => {
const { containerRef, isPlaying, playPause } = useWaveSurferPlayer(audioUrl);
return (
<Box sx={{ display: "flex", alignItems: "center", gap: 1, justifyContent: "center" }}>
<div ref={containerRef} style={{ width: 180 }} />
<IconButton aria-label="play-pause" onClick={playPause}>
{isPlaying ? <PauseIcon sx={{ color: "#d32f2f" }} /> : <PlayArrowIcon sx={{ color: "#1b4332" }} />}
</IconButton>
</Box>
);
};
export default Player;

View File

@@ -0,0 +1,158 @@
"use client";
import { Button, FormControl, Grid, InputLabel, MenuItem, Select } from "@mui/material";
import LtrTextField from "@/core/components/LtrTextField";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { object, string } from "yup";
import { GET_OPERATOR_DIALOGUE_SUPERVISOR } from "@/core/utils/routes";
import StyledForm from "@/core/components/StyledForm";
import useRequest from "@/lib/hooks/useRequest";
import { useEffect } from "react";
const RateMessage = ({ date, messageId, initialValues, mutate }) => {
const requestServer = useRequest({ notificationSuccess: true });
const defaultValues = {
supervisor_id: initialValues?.supervisor_id ?? "",
is_good: initialValues?.is_good ?? "",
description: initialValues?.description ?? "",
};
const validationSchema = object().shape({
supervisor_id: string().required("کارشناس بررسی کننده را مشخص کنید"),
is_good: string().required("نحوه پاسخگویی را مشخص کنید"),
description: string().optional(),
});
const {
control,
handleSubmit,
reset,
formState: { isSubmitting, errors, isDirty, isValid },
} = useForm({ defaultValues, resolver: yupResolver(validationSchema) });
const onSubmit = async (data) => {
const fixData = {
date: date,
cdr_unique_id: messageId,
supervisor_id: data.supervisor_id,
is_good: parseInt(data.is_good),
...(data.description !== "" && { description: data.description }),
};
try {
await requestServer(`${GET_OPERATOR_DIALOGUE_SUPERVISOR}/rate`, "post", {
data: fixData,
});
mutate();
reset({
supervisor_id: initialValues?.supervisor_id ?? "",
is_good: initialValues?.is_good ?? "",
description: initialValues?.description ?? "",
});
} catch (error) {}
};
useEffect(() => {
reset({
supervisor_id: initialValues?.supervisor_id ?? "",
is_good: initialValues?.is_good ?? "",
description: initialValues?.description ?? "",
});
}, [initialValues.supervisor_id, initialValues.is_good, initialValues.description]);
return (
<StyledForm id="monitoringForm" onSubmit={handleSubmit(onSubmit)} style={{ width: "100%", height: "100%" }}>
<Grid container spacing={2} sx={{ flexWrap: "nowrap", alignItems: "start" }}>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
variant="outlined"
label="کارشناس بررسی کننده"
size="small"
sx={{
width: "180px",
}}
InputProps={{ sx: { fontSize: "12px" } }}
InputLabelProps={{ sx: { fontSize: "12px" } }}
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name="supervisor_id"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<FormControl fullWidth error={!!error} variant="outlined">
<InputLabel id="label-message-quality" size="small" sx={{ fontSize: "12px" }}>
نحوه پاسخگویی
</InputLabel>
<Select
variant="outlined"
labelId="label-message-quality"
id="is_good"
name="is_good"
size="small"
sx={{ width: "100px", fontSize: "12px" }}
value={field.value}
label="نحوه پاسخگویی"
onChange={(e) => field.onChange(e.target.value)}
>
<MenuItem value={0}>نامناسب</MenuItem>
<MenuItem value={1}>مناسب</MenuItem>
</Select>
</FormControl>
);
}}
name="is_good"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
variant="outlined"
label="توضیحات"
size="small"
sx={{
width: "180px",
}}
InputProps={{ sx: { fontSize: "12px" } }}
InputLabelProps={{ sx: { fontSize: "12px" } }}
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name="description"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Button
type="submit"
disabled={isSubmitting || !isDirty || !isValid}
size="medium"
autoFocus
variant="contained"
>
{isSubmitting ? "درحال ثبت..." : "ثبت"}
</Button>
</Grid>
</Grid>
</StyledForm>
);
};
export default RateMessage;

View File

@@ -0,0 +1,16 @@
"use client";
import PageTitle from "@/core/components/PageTitle";
import { Stack } from "@mui/material";
import DataTable from "./DataTable";
const OperatorDialoguePage = () => {
return (
<Stack spacing={1}>
<PageTitle title={"ارزیابی صدای کارشناسان"} />
<DataTable />
</Stack>
);
};
export default OperatorDialoguePage;

View File

@@ -0,0 +1,167 @@
"use client";
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_OPERATOR_PEOPLE_MESSAGE_SUPERVISOR } from "@/core/utils/routes";
import { Box } from "@mui/material";
import { useMemo } from "react";
import moment from "jalali-moment";
import Player from "./Player";
import CloseIcon from '@mui/icons-material/Close';
import CheckIcon from '@mui/icons-material/Check';
import RateMessage from "./RateMessage";
const DataTable = () => {
const columns = useMemo(
() => [
{
accessorKey: "id",
header: "کد یکتا",
id: "id",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
header: "تاریخ",
id: "date",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) =>
row.original.date ? moment(row.original.date).locale("fa").format("yyyy/MM/DD") : <>-</>,
},
{
header: "ساعت",
id: "time",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) =>
row.original.date ? moment(row.original.date).locale("fa").format("HH:mm") : <>-</>,
},
{
accessorKey: "caller_phone_number",
header: "شماره تماس",
id: "caller_phone_number",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
header: "پخش صدا",
id: "player",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) => <Player audioUrl={row.original.voice} messageId={row.original.id} />,
},
{
header: "پخش صدا",
id: "is_listen",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) => {
const isListen = row.original.is_listen;
const Icon = isListen ? CheckIcon : CloseIcon;
const color = isListen ? "success.main" : "error.main";
return (
<Box sx={{ display: "flex", justifyContent: "center" }}>
<Icon sx={{ color }} />
</Box>
);
}
},
{
accessorKey: "listen_at",
header: "زمان شنیده شدن",
id: "listen_at",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "review_duration",
header: "زمان بررسی (دقیقه)",
id: "review_duration",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
header: "ارزیابی",
id: "rate",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ cell, row, table }) => {
const mutate = table.options.meta?.mutate;
return (
<RateMessage
mutate={mutate}
messageId={row.original.id}
initialValues={{
savaneh_id: row.original.savaneh_id,
action_id: row.original.action_id,
description: row.original.description,
}}
/>
);
},
},
],
[]
);
return (
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
<DataTableWithAuth
need_filter={true}
columns={columns}
table_url={GET_OPERATOR_PEOPLE_MESSAGE_SUPERVISOR}
page_name={"operatorDialoguePage"}
table_name={"operatorDialogueList"}
positionActionsColumn={"last"}
/>
</Box>
);
};
export default DataTable;

View File

@@ -0,0 +1,45 @@
"use client";
import React from "react";
import { IconButton, Box, Typography } from "@mui/material";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import PauseIcon from "@mui/icons-material/Pause";
import useWaveSurferPlayer from "@/lib/hooks/useWaveSurferPlayer";
function formatTime(seconds) {
if (!seconds || isNaN(seconds)) return "0:00";
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${minutes}:${secs.toString().padStart(2, "0")}`;
}
const Player = ({ audioUrl }) => {
const { containerRef, isPlaying, playPause, currentTime, duration } =
useWaveSurferPlayer(audioUrl);
return (
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 1,
justifyContent: "center",
}}
>
<div ref={containerRef} style={{ width: 180 }} />
<IconButton aria-label="play-pause" onClick={playPause}>
{isPlaying ? (
<PauseIcon sx={{ color: "#d32f2f" }} />
) : (
<PlayArrowIcon sx={{ color: "#1b4332" }} />
)}
</IconButton>
<Typography variant="body2" sx={{ minWidth: 60, textAlign: "right" }}>
{formatTime(currentTime)} / {formatTime(duration)}
</Typography>
</Box>
);
};
export default Player;

View File

@@ -0,0 +1,160 @@
"use client";
import { Button, FormControl, FormHelperText, Grid, InputLabel, MenuItem, OutlinedInput, Select } from "@mui/material";
import LtrTextField from "@/core/components/LtrTextField";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { object, string, number } from "yup";
import { GET_SYSTEM_MESSAGES_SUPERVISOR } from "@/core/utils/routes";
import StyledForm from "@/core/components/StyledForm";
import useRequest from "@/lib/hooks/useRequest";
import usePermissionsList from "@/lib/hooks/usePermissionsList";
import { useEffect } from "react";
const RateMessage = ({ messageId, initialValues, mutate }) => {
const requestServer = useRequest({ notificationSuccess: true });
const defaultValues = {
savaneh_id: initialValues?.savaneh_id ?? "",
action_id: initialValues?.action_id ?? "",
description: initialValues?.description ?? "",
};
const validationSchema = object().shape({
savaneh_id: string().required("وضعیت کیفیت پیام را مشخص کنید"),
action_id: string().optional(),
description: string().optional(),
});
const {
control,
handleSubmit,
reset,
formState: { isSubmitting, errors, isDirty, isValid },
} = useForm({ defaultValues, resolver: yupResolver(validationSchema) });
const onSubmit = async (data) => {
const fixData = {
savaneh_id: parseInt(data.savaneh_id),
...(data.action_id !== "" && { action_id: data.action_id }),
...(data.description !== "" && { description: data.description }),
};
try {
await requestServer(`${GET_SYSTEM_MESSAGES_SUPERVISOR}/${messageId}`, "post", {
data: fixData,
});
mutate();
reset({
savaneh_id: initialValues?.savaneh_id ?? "",
action_id: initialValues?.action_id ?? "",
description: initialValues?.description ?? "",
});
} catch (error) {}
};
useEffect(() => {
reset({
savaneh_id: initialValues?.savaneh_id ?? "",
action_id: initialValues?.action_id ?? "",
description: initialValues?.description ?? "",
});
}, [initialValues.savaneh_id, initialValues.action_id, initialValues.description]);
return (
<StyledForm id="monitoringForm" onSubmit={handleSubmit(onSubmit)} style={{ width: "100%", height: "100%" }}>
<Grid container spacing={2} sx={{ flexWrap: "nowrap", alignItems: "start" }}>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<FormControl fullWidth error={!!error} variant="outlined">
<InputLabel id="label-message-quality" size="small" sx={{ fontSize: "12px" }}>
پیگیری
</InputLabel>
<Select
variant="outlined"
labelId="label-message-quality"
id="savaneh_id"
name="savaneh_id"
size="small"
sx={{ width: "100px", fontSize: "12px" }}
value={field.value}
label="پیگیری"
onChange={(e) => field.onChange(e.target.value)}
>
<MenuItem value={1}>عدم نیاز به ثبت (مشکل راه)</MenuItem>
<MenuItem value={2}>عدم نیاز به ثبت (مشکل حمل و نقلی)</MenuItem>
<MenuItem value={3}>عدم نیاز به ثبت (مشکل عوارضی)</MenuItem>
<MenuItem value={4}>ثبت سوانح (مشکل راه)</MenuItem>
<MenuItem value={5}>ثبت سوانح (مشکل حمل و نقلی)</MenuItem>
</Select>
</FormControl>
);
}}
name="savaneh_id"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
variant="outlined"
label="کد واقعه"
size="small"
sx={{
width: "180px",
}}
InputProps={{ sx: { fontSize: "12px" } }}
InputLabelProps={{ sx: { fontSize: "12px" } }}
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name="action_id"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
variant="outlined"
label="توضیحات"
size="small"
sx={{
width: "180px",
}}
InputProps={{ sx: { fontSize: "12px" } }}
InputLabelProps={{ sx: { fontSize: "12px" } }}
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name="description"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Button
type="submit"
disabled={isSubmitting || !isDirty || !isValid}
size="medium"
autoFocus
variant="contained"
>
{isSubmitting ? "درحال ثبت..." : "ثبت"}
</Button>
</Grid>
</Grid>
</StyledForm>
);
};
export default RateMessage;

View File

@@ -0,0 +1,16 @@
"use client";
import PageTitle from "@/core/components/PageTitle";
import { Stack } from "@mui/material";
import DataTable from "./DataTable";
const PeopleMessagePage = () => {
return (
<Stack spacing={1}>
<PageTitle title={"ارزیابی پیام های مردمی"} />
<DataTable />
</Stack>
);
};
export default PeopleMessagePage;

View File

@@ -23,6 +23,18 @@ const DataTable = () => {
grow: false, grow: false,
size: 100, size: 100,
}, },
{
accessorKey: "type_name",
header: "نوع",
id: "type_name",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{ {
header: "تاریخ", header: "تاریخ",
id: "date", id: "date",
@@ -50,6 +62,7 @@ const DataTable = () => {
row.original.date ? moment(row.original.date).locale("fa").format("HH:mm") : <>-</>, row.original.date ? moment(row.original.date).locale("fa").format("HH:mm") : <>-</>,
}, },
{ {
accessorKey: "duration",
header: "طول تماس ( ثانیه )", header: "طول تماس ( ثانیه )",
id: "duration", id: "duration",
enableColumnFilter: true, enableColumnFilter: true,

View File

@@ -0,0 +1,45 @@
"use client";
import React from "react";
import { IconButton, Box, Typography } from "@mui/material";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import PauseIcon from "@mui/icons-material/Pause";
import useWaveSurferPlayer from "@/lib/hooks/useWaveSurferPlayer";
function formatTime(seconds) {
if (!seconds || isNaN(seconds)) return "0:00";
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${minutes}:${secs.toString().padStart(2, "0")}`;
}
const Player = ({ audioUrl }) => {
const { containerRef, isPlaying, playPause, currentTime, duration } =
useWaveSurferPlayer(audioUrl);
return (
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 1,
justifyContent: "center",
}}
>
<div ref={containerRef} style={{ width: 180 }} />
<IconButton aria-label="play-pause" onClick={playPause}>
{isPlaying ? (
<PauseIcon sx={{ color: "#d32f2f" }} />
) : (
<PlayArrowIcon sx={{ color: "#1b4332" }} />
)}
</IconButton>
<Typography variant="body2" sx={{ minWidth: 60, textAlign: "right" }}>
{formatTime(currentTime)} / {formatTime(duration)}
</Typography>
</Box>
);
};
export default Player;

View File

@@ -0,0 +1,16 @@
"use client";
import PageTitle from "@/core/components/PageTitle";
import { Stack } from "@mui/material";
import DataTable from "./DataTable";
const SystemMessagesPage = () => {
return (
<Stack spacing={1}>
<PageTitle title={"ارزیابی پیام های سیستمی"} />
<DataTable />
</Stack>
);
};
export default SystemMessagesPage;

View File

@@ -138,27 +138,51 @@ export const pageMenu = [
hasSubitems: true, hasSubitems: true,
Subitems: [ Subitems: [
{ {
id: "monitoring-operator-dialogue", id: "monitoring-operator-dialogue-supervisor",
label: "ارزیابی صدای کارشناسان",
type: "page",
icon: <VoiceChatIcon sx={{ width: "inherit", height: "inherit" }} />,
route: "/dashboard/monitoring/operator-dialogue/supervisor",
permissions: ["all"],
},
{
id: "monitoring-operator-dialogue-operator",
label: "صدای کارشناسان", label: "صدای کارشناسان",
type: "page", type: "page",
icon: <VoiceChatIcon sx={{ width: "inherit", height: "inherit" }} />, icon: <VoiceChatIcon sx={{ width: "inherit", height: "inherit" }} />,
route: "/dashboard/monitoring/operator-dialogue", route: "/dashboard/monitoring/operator-dialogue/operator",
permissions: ["all"], permissions: ["all"],
}, },
{ {
id: "monitoring-system-messages", id: "monitoring-system-messages-supervisor",
label: "ارزیابی پیام های سیستمی",
type: "page",
icon: <PointOfSaleIcon sx={{ width: "inherit", height: "inherit" }} />,
route: "/dashboard/monitoring/system-messages/supervisor",
permissions: ["all"],
},
{
id: "monitoring-system-messages-operator",
label: "پیام های سیستمی", label: "پیام های سیستمی",
type: "page", type: "page",
icon: <PointOfSaleIcon sx={{ width: "inherit", height: "inherit" }} />, icon: <PointOfSaleIcon sx={{ width: "inherit", height: "inherit" }} />,
route: "/dashboard/monitoring/system-messages", route: "/dashboard/monitoring/system-messages/operator",
permissions: ["all"], permissions: ["all"],
}, },
{ {
id: "monitoring-people-messages", id: "monitoring-people-messages-supervisor",
label: "ارزیابی پیام های مردمی",
type: "page",
icon: <ThreePIcon sx={{ width: "inherit", height: "inherit" }} />,
route: "/dashboard/monitoring/people-messages/supervisor",
permissions: ["all"],
},
{
id: "monitoring-people-messages-operator",
label: "پیام های مردمی", label: "پیام های مردمی",
type: "page", type: "page",
icon: <ThreePIcon sx={{ width: "inherit", height: "inherit" }} />, icon: <ThreePIcon sx={{ width: "inherit", height: "inherit" }} />,
route: "/dashboard/monitoring/people-messages", route: "/dashboard/monitoring/people-messages/operator",
permissions: ["all"], permissions: ["all"],
}, },
], ],

View File

@@ -26,6 +26,12 @@ export const GET_COUNTRY_KEYPRESS_REPORT = api + "/cumulative_keypress_stats";
export const GET_SYSTEM_MESSAGES_SUPERVISOR = api + "/system_messages/supervisor"; export const GET_SYSTEM_MESSAGES_SUPERVISOR = api + "/system_messages/supervisor";
export const GET_SYSTEM_MESSAGES_OPERATOR = api + "/system_messages/operator"; export const GET_SYSTEM_MESSAGES_OPERATOR = api + "/system_messages/operator";
export const GET_OPERATOR_DIALOGUE_SUPERVISOR = api + "/dialogs/supervisor";
export const GET_OPERATOR_DIALOGUE_OPERATOR = api + "/dialogs/operator";
export const GET_OPERATOR_PEOPLE_MESSAGE_SUPERVISOR = api + "/people_messages/supervisor";
export const GET_OPERATOR_PEOPLE_MESSAGE_OPERATOR = api + "/people_messages/operator";
// export const GET_PROVINCE_CUMULATIVE_CALLS_TOTAL_REPORT = api + "/report/cumulative_total"; // export const GET_PROVINCE_CUMULATIVE_CALLS_TOTAL_REPORT = api + "/report/cumulative_total";
export const GET_PROVINCE_CUMULATIVE_CALLS_DAILY_REPORT = api + "/report/cumulative"; export const GET_PROVINCE_CUMULATIVE_CALLS_DAILY_REPORT = api + "/report/cumulative";
export const GET_PROVINCE_PEOPLE_MESSAGES_REPORT = api + "/report/people_messages"; export const GET_PROVINCE_PEOPLE_MESSAGES_REPORT = api + "/report/people_messages";

View File

@@ -6,7 +6,11 @@ const playerRegistry = new Set();
export default function useWaveSurferPlayer(audioUrl, options = {}) { export default function useWaveSurferPlayer(audioUrl, options = {}) {
const containerRef = useRef(null); const containerRef = useRef(null);
const wavesurferRef = useRef(null); const wavesurferRef = useRef(null);
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
const [isLoaded, setIsLoaded] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
useEffect(() => { useEffect(() => {
if (!containerRef.current) return; if (!containerRef.current) return;
@@ -17,7 +21,6 @@ export default function useWaveSurferPlayer(audioUrl, options = {}) {
progressColor: "#1b4332", progressColor: "#1b4332",
cursorColor: "#d32f2f", cursorColor: "#d32f2f",
height: 40, height: 40,
url: audioUrl,
...options, ...options,
}); });
@@ -26,12 +29,24 @@ export default function useWaveSurferPlayer(audioUrl, options = {}) {
wavesurferRef.current.on("finish", () => setIsPlaying(false)); wavesurferRef.current.on("finish", () => setIsPlaying(false));
wavesurferRef.current.on("play", () => setIsPlaying(true)); wavesurferRef.current.on("play", () => setIsPlaying(true));
wavesurferRef.current.on("pause", () => setIsPlaying(false)); wavesurferRef.current.on("pause", () => setIsPlaying(false));
wavesurferRef.current.on("ready", () => {
setIsLoaded(true);
setDuration(wavesurferRef.current.getDuration());
});
wavesurferRef.current.on("audioprocess", () => {
setCurrentTime(wavesurferRef.current.getCurrentTime());
});
wavesurferRef.current.on("seek", () => {
setCurrentTime(wavesurferRef.current.getCurrentTime());
});
return () => { return () => {
playerRegistry.delete(wavesurferRef.current); playerRegistry.delete(wavesurferRef.current);
wavesurferRef.current?.destroy(); wavesurferRef.current?.destroy();
}; };
}, [audioUrl]); }, []);
const playPause = () => { const playPause = () => {
if (!wavesurferRef.current) return; if (!wavesurferRef.current) return;
@@ -42,7 +57,16 @@ export default function useWaveSurferPlayer(audioUrl, options = {}) {
} }
}); });
wavesurferRef.current.playPause(); if (!isLoaded) {
setIsLoaded(true);
wavesurferRef.current.load(audioUrl);
wavesurferRef.current.once("ready", () => {
setDuration(wavesurferRef.current.getDuration());
wavesurferRef.current.play();
});
} else {
wavesurferRef.current.playPause();
}
}; };
const stop = () => { const stop = () => {
@@ -59,6 +83,8 @@ export default function useWaveSurferPlayer(audioUrl, options = {}) {
playPause, playPause,
stop, stop,
seekTo, seekTo,
currentTime,
duration,
wavesurfer: wavesurferRef.current, wavesurfer: wavesurferRef.current,
}; };
} }