first commit

This commit is contained in:
2026-05-23 13:22:10 +03:30
parent 5f9ee72174
commit 6591a52f27
52 changed files with 3937 additions and 133 deletions

32
ui/DateFilter.tsx Normal file
View File

@@ -0,0 +1,32 @@
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { TextField, Box } from "@mui/material";
const DateFilters = ({
startDate,
setStartDate,
endDate,
setEndDate,
}: {
startDate: any;
setStartDate: any;
endDate: any;
setEndDate: any;
}) => {
return (
<Box sx={{ display: "flex", gap: 2, my: 2 }}>
<DatePicker
label="از تاریخ"
value={startDate}
onChange={(newValue) => setStartDate(newValue)}
slotProps={{ textField: { fullWidth: true } }}
/>
<DatePicker
label="تا تاریخ"
value={endDate}
onChange={(newValue) => setEndDate(newValue)}
slotProps={{ textField: { fullWidth: true } }}
/>
</Box>
);
};
export default DateFilters;

80
ui/DeleteConfirmModal.tsx Normal file
View File

@@ -0,0 +1,80 @@
"use client";
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Typography,
IconButton,
} from "@mui/material";
import WarningAmberIcon from "@mui/icons-material/WarningAmber";
import CloseIcon from "@mui/icons-material/Close";
export default function DeleteConfirmModal({
open,
onClose,
onConfirm,
title = "آیا از حذف این مورد مطمئن هستید؟",
description = "بعد از حذف، امکان بازگردانی وجود ندارد.",
}: {
open: boolean;
onClose: () => void;
onConfirm: () => void;
title?: string;
description?: string;
}) {
return (
<Dialog open={open} onClose={onClose} maxWidth="xs">
<DialogTitle className="flex items-center justify-between text-slate-800">
<span className="font-semibold">تأیید حذف</span>
<IconButton onClick={onClose} size="small">
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent className="flex flex-col items-center text-center space-y-3 pt-4">
<WarningAmberIcon className="text-yellow-500" sx={{ fontSize: 50 }} />
<Typography className="text-lg font-semibold text-slate-700">
{title}
</Typography>
<Typography className="text-slate-500 text-sm leading-relaxed">
{description}
</Typography>
</DialogContent>
<DialogActions className="flex justify-between px-4 pb-3 mt-2">
<Button
onClick={onClose}
variant="outlined"
sx={{
borderRadius: "8px",
color: "#475569",
borderColor: "#cbd5e1",
"&:hover": {
backgroundColor: "#f1f5f9",
borderColor: "#94a3b8",
},
}}
>
لغو
</Button>
<Button
onClick={onConfirm}
variant="contained"
sx={{
borderRadius: "8px",
backgroundColor: "#dc2626",
"&:hover": { backgroundColor: "#b91c1c" },
}}
>
حذف آیتم
</Button>
</DialogActions>
</Dialog>
);
}

30
ui/ReactQueryProvider.tsx Normal file
View File

@@ -0,0 +1,30 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState } from "react";
export default function ReactQueryProvider({
children,
}: {
children: React.ReactNode;
}) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60,
retry: 1,
refetchOnWindowFocus: false,
},
mutations: {
retry: 1,
},
},
}),
);
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}

31
ui/SearchBox.tsx Normal file
View File

@@ -0,0 +1,31 @@
import { ChangeEventHandler } from 'react';
import { TextField } from '@mui/material';
const SearchBox = ({ placeholder, onChange }:{placeholder:string,onChange:ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement, Element>}) => {
return (
<TextField
variant="outlined"
placeholder={placeholder || "جستجو کنید..."}
onChange={onChange}
fullWidth
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: '16px', // گرد کردن لبه‌ها برای ظاهر مدرن
backgroundColor: '#f5f5f5',
'& fieldset': {
borderColor: 'transparent', // حذف خط پیش‌فرض
},
'&:hover fieldset': {
borderColor: '#1976d2',
},
'&.Mui-focused fieldset': {
borderColor: '#1976d2',
},
},
}}
/>
);
};
export default SearchBox;

83
ui/Test.tsx Normal file
View File

@@ -0,0 +1,83 @@
import {
Box,
Card,
CardContent,
Typography,
Button,
Stack,
} from "@mui/material";
import VisibilityIcon from "@mui/icons-material/Visibility";
import FileDownloadIcon from "@mui/icons-material/FileDownload";
const DataCategories = () => {
// لیست کارت‌ها (میتوانید این را از دیتابیس بگیرید)
const categories = [
{ id: 1, title: "آماركلي" },
{ id: 2, title: "تیکت‌های در انتظار" },
{ id: 3, title: "تيكت هاي بحراني" },
{ id: 4, title: "گزارش‌های ماهانه" },
{ id: 5, title: "گزارش‌های ماهانه" },
{ id: 6, title: "گزارش‌های ماهانه" },
{ id: 7, title: "گزارش‌های ماهانه" },
{ id: 8, title: "گزارش‌های ماهانه" },
{ id: 9, title: "گزارش‌های ماهانه" },
{ id: 10, title: "گزارش‌های ماهانه" },
{ id: 11, title: "گزارش‌های ماهانه" },
{ id: 12, title: "گزارش‌های ماهانه" },
{ id: 13, title: "گزارش‌های ماهانه" },
];
return (
<Box
sx={{
display: "grid",
// ریسپانسیو بودن: در موبایل ۱ ستون، در تبلت ۲، در دسکتاپ ۴ ستون
gridTemplateColumns: {
xs: "1fr",
sm: "1fr 1fr",
md: "repeat(6, 1fr)",
},
gap: 3,
}}
>
{categories.map((item) => (
<Card
key={item.id}
sx={{
aspectRatio: "1/1", // مربع کردن کارت
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
transition: "0.3s",
"&:hover": { boxShadow: 6, transform: "translateY(-5px)" },
}}
>
<CardContent>
<Typography variant="h6" align="center" gutterBottom>
{item.title}
</Typography>
</CardContent>
<Stack spacing={1} sx={{ p: 2 }}>
{/* <Button
variant="contained"
startIcon={<VisibilityIcon />}
fullWidth
>
مشاهده
</Button> */}
<Button
variant="outlined"
startIcon={<FileDownloadIcon />}
fullWidth
>
اکسل
</Button>
</Stack>
</Card>
))}
</Box>
);
};
export default DataCategories;

115
ui/TicketDetailModel.tsx Normal file
View File

@@ -0,0 +1,115 @@
"use client";
import {
Dialog,
DialogTitle,
DialogContent,
IconButton,
Typography,
Chip,
Divider,
} from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import { ticketPriorities, ticketStatuses } from "@/core/constant";
import { TicketInterface } from "@/core/types";
export default function TicketDetailModal({
open,
onClose,
ticket,
}: {
open: boolean;
onClose: () => void;
ticket: TicketInterface | null;
}) {
if (!ticket) return null;
const status = ticketStatuses.find((p) => p.id === ticket?.status);
const priority = ticketPriorities.find((p) => p.id === ticket?.priority);
return (
<Dialog open={open} onClose={onClose} fullWidth maxWidth="sm">
<DialogTitle className="flex justify-between items-center text-slate-800">
<span>جزئیات تیکت #{ticket.id.slice(-6)}</span>
<IconButton onClick={onClose} size="small">
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent dividers>
<div className="space-y-4">
<div>
<Typography variant="subtitle2" className="text-slate-500">
واحد / بخش
</Typography>
<Typography className="font-semibold text-slate-900">
{ticket.department?.displayName}
</Typography>
</div>
<div>
<Typography variant="subtitle2" className="text-slate-500">
كاربر
</Typography>
<Typography className="font-semibold text-slate-900">
{ticket.createdBy}
</Typography>
</div>
<div>
<Typography variant="subtitle2" className="text-slate-500">
ارجاع به
</Typography>
<Typography className="font-semibold text-slate-900">
{ticket.assignee.fullname}
</Typography>
</div>
<div className="flex items-center justify-start gap-4">
<div>
<Typography variant="subtitle2" className="text-slate-500">
وضعیت
</Typography>
<Chip
label={status?.displayName}
className={`mt-1 bg-blue-100 text-blue-700 font-medium`}
/>
</div>
<div>
<Typography variant="subtitle2" className="text-slate-500">
اولویت
</Typography>
<Chip
label={priority?.displayName}
className={`mt-1 bg-red-100 text-red-700 font-medium`}
/>
</div>
<div>
<Typography variant="subtitle2" className="text-slate-500">
محل وقوع مشكل
</Typography>
<Chip
label={ticket?.location}
className={`mt-1 bg-blue-100 text-blue-700 font-medium`}
/>
</div>
</div>
<Divider />
<div>
<Typography variant="subtitle2" className="text-slate-500">
توضیحات
</Typography>
<Typography className="text-slate-700 mt-2 leading-relaxed bg-slate-50 p-3 rounded-lg border border-slate-100">
{ticket.description || "توضیحی ثبت نشده است."}
</Typography>
</div>
<div>
<Typography variant="subtitle2" className="text-slate-500">
اقدام كارشناس
</Typography>
<Typography className="text-slate-700 mt-2 leading-relaxed bg-slate-50 p-3 rounded-lg border border-slate-100">
{ticket.helpdeskAction || "توضیحی ثبت نشده است."}
</Typography>
</div>
</div>
</DialogContent>
</Dialog>
);
}

158
ui/form/LoginForm.tsx Normal file
View File

@@ -0,0 +1,158 @@
"use client";
import { useState } from "react";
import { Box, Button, TextField, Paper } from "@mui/material";
import { handleAxiosError } from "@/core/utils";
import { toast } from "react-toastify";
import { useRouter } from "next/navigation";
import { loginUser } from "@/services/api/auth.api";
export default function LoginForm() {
const [showPassword, setShowPassword] = useState(false);
const router = useRouter();
const [form, setForm] = useState({
username: "",
password: "",
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setForm({
...form,
[e.target.name]: e.target.value,
});
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const { message } = await loginUser(form);
toast.success(message);
router.push("/");
} catch (error) {
toast.error(handleAxiosError(error));
}
};
return (
<Box
sx={{
minHeight: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Paper
elevation={0}
sx={{
width: 420,
p: 5,
borderRadius: 4,
backdropFilter: "blur(10px)",
background: "rgba(255,255,255,0.9)",
boxShadow: "0 20px 60px rgba(0,0,0,0.15)",
}}
>
<h5 className="font-medium text-center mb-5 text-xl">
ورود به سيستم ثبت تيكت ها
</h5>
<form onSubmit={handleSubmit}>
<TextField
fullWidth
label="نام کاربری"
name="username"
value={form.username}
onChange={handleChange}
margin="normal"
// InputProps={{
// startAdornment: (
// <InputAdornment position="start">
// <Person sx={{ color: "#1976d2" }} />
// </InputAdornment>
// ),
// }}
sx={{
mb: 2,
"& .MuiOutlinedInput-root": {
borderRadius: 3,
background: "#f9fbff",
"& fieldset": {
borderColor: "#dbeafe",
},
"&:hover fieldset": {
borderColor: "#90caf9",
},
"&.Mui-focused fieldset": {
borderColor: "#1976d2",
borderWidth: "2px",
},
},
}}
/>
<TextField
fullWidth
label="رمز عبور"
name="password"
type={showPassword ? "text" : "password"}
value={form.password}
onChange={handleChange}
margin="normal"
// InputProps={{
// startAdornment: (
// <InputAdornment position="start">
// <Lock sx={{ color: "#1976d2" }} />
// </InputAdornment>
// ),
// endAdornment: (
// <InputAdornment position="end">
// <IconButton onClick={() => setShowPassword(!showPassword)}>
// {showPassword ? <VisibilityOff /> : <Visibility />}
// </IconButton>
// </InputAdornment>
// ),
// }}
sx={{
mb: 3,
"& .MuiOutlinedInput-root": {
borderRadius: 3,
background: "#f9fbff",
"& fieldset": {
borderColor: "#dbeafe",
},
"&:hover fieldset": {
borderColor: "#90caf9",
},
"&.Mui-focused fieldset": {
borderColor: "#1976d2",
borderWidth: "2px",
},
},
}}
/>
<Button
fullWidth
type="submit"
variant="contained"
sx={{
py: 1.5,
borderRadius: 3,
fontSize: 16,
fontWeight: 600,
background: "linear-gradient(90deg,#1976d2,#42a5f5,#64b5f6)",
boxShadow: "0 10px 25px rgba(25,118,210,0.4)",
"&:hover": {
background: "linear-gradient(90deg,#1565c0,#1e88e5,#42a5f5)",
},
}}
>
ورود
</Button>
</form>
</Paper>
</Box>
);
}

268
ui/form/TicketForm.tsx Normal file
View File

@@ -0,0 +1,268 @@
"use client";
import { useEffect, useState } from "react";
import {
Box,
Paper,
Typography,
TextField,
MenuItem,
Button,
Grid,
} from "@mui/material";
import {
hospitalSoftwares,
requestType,
ticketPriorities,
ticketStatuses,
} from "@/core/constant";
import { toast } from "react-toastify";
import { handleAxiosError } from "@/core/utils";
import { createTicket } from "@/services/api/ticket.api";
interface DepartmentType {
id: string;
slug: string;
displayName: string;
}
export interface IUserListItem {
id: string;
fullname: string;
nationalCode: string;
mobile: string;
roleId: string;
createdAt: Date;
updatedAt: Date;
}
export default function TicketForm({
departments,
users,
}: {
departments: DepartmentType[];
users: IUserListItem[];
}) {
const [showFieldRelatedSystem, setShowFieldRelatedSystem] = useState(false);
const [form, setForm] = useState({
createdBy: "",
departmentId: "",
internalPhone: "",
requestType: "",
priority: "medium",
description: "",
relatedSystem: "",
location: "",
helpdeskAction: "",
assignedTo: "",
status: "open",
finalNotes: "",
});
const handleChange = (e: any) => {
setForm({
...form,
[e.target.name]: e.target.value,
});
};
const handleSubmit = async (e: any) => {
e.preventDefault();
try {
const { message } = await createTicket(form);
toast.success(message);
} catch (error) {
toast.error(handleAxiosError(error));
}
};
useEffect(() => {
if (form.requestType === "1") {
form.relatedSystem = "";
setShowFieldRelatedSystem(true);
} else {
form.relatedSystem = "";
setShowFieldRelatedSystem(false);
}
}, [form.requestType]);
return (
<div className=" h-full w-full">
<div className="bg-white p-6 rounded-2xl">
<h5 className="mb-5 text-center text-xl font-bold">فرم تيكت</h5>
<form onSubmit={handleSubmit}>
<div className="space-y-5">
<div className="flex items-center justify-start gap-x-5">
<TextField
fullWidth
label="نام و نام خانوادگي كاربر"
name="createdBy"
value={form.createdBy}
onChange={handleChange}
/>
<TextField
select
fullWidth
size="medium"
label="واحد / بخش"
name="departmentId"
value={form.departmentId}
onChange={handleChange}
>
{departments.map((dep) => (
<MenuItem key={dep.id} value={dep.id}>
{dep.displayName}
</MenuItem>
))}
</TextField>
<TextField
fullWidth
label="تلفن داخلي"
name="internalPhone"
value={form.internalPhone}
onChange={handleChange}
/>
</div>
<div className="flex items-center justify-start gap-x-5">
<TextField
select
fullWidth
label="نوع درخواست"
name="requestType"
value={form.requestType}
onChange={handleChange}
>
{requestType.map((dep) => (
<MenuItem key={dep.id} value={dep.id}>
{dep.displayName}
</MenuItem>
))}
</TextField>
{showFieldRelatedSystem && (
<TextField
select
fullWidth
size="medium"
label="نرم افزار مرتبط"
name="relatedSystem"
value={form.relatedSystem}
onChange={handleChange}
>
{hospitalSoftwares.map((item) => (
<MenuItem value={item.id}>{item.displayName}</MenuItem>
))}
</TextField>
)}
<TextField
fullWidth
label="محل وقوع مشكل"
name="location"
value={form.location}
onChange={handleChange}
/>
</div>
<div className="flex flex-col gap-y-5">
<TextField
multiline
rows={4}
fullWidth
label="توضيحات"
name="description"
value={form.description}
onChange={handleChange}
/>
<TextField
multiline
rows={3}
fullWidth
label="اقدام helpdesk"
name="helpdeskAction"
value={form.helpdeskAction}
onChange={handleChange}
/>
</div>
<div className="flex items-center justify-start gap-x-5">
<TextField
select
fullWidth
label="ارجاع به"
name="assignedTo"
value={form.assignedTo}
onChange={handleChange}
>
{users.map((dep) => (
<MenuItem key={dep.id} value={dep.id}>
{dep.fullname}
</MenuItem>
))}
</TextField>
<TextField
select
fullWidth
size="medium"
label="اولويت درخواست"
name="priority"
value={form.priority}
onChange={handleChange}
>
{ticketPriorities?.map((p: any) => (
<MenuItem key={p.id} value={p.id}>
{p.displayName}
</MenuItem>
))}
</TextField>
<TextField
select
fullWidth
label="وضعيت درخواست"
name="status"
value={form.status}
onChange={handleChange}
>
{ticketStatuses?.map((p: any) => (
<MenuItem key={p.id} value={p.id}>
{p.displayName}
</MenuItem>
))}
</TextField>
</div>
<div>
<TextField
multiline
rows={3}
fullWidth
label="توضيحات نهايي"
name="finalNotes"
value={form.finalNotes}
onChange={handleChange}
/>
</div>
<Button
fullWidth
variant="contained"
type="submit"
sx={{
py: 1.5,
borderRadius: 3,
fontSize: 16,
fontWeight: 600,
background: "linear-gradient(90deg,#1976d2,#42a5f5,#64b5f6)",
boxShadow: "0 10px 25px rgba(25,118,210,0.4)",
"&:hover": {
background: "linear-gradient(90deg,#1565c0,#1e88e5,#42a5f5)",
},
}}
>
ثبت تيكت
</Button>
</div>
</form>
</div>
</div>
);
}

82
ui/layout/Sidebar.tsx Normal file
View File

@@ -0,0 +1,82 @@
import { Apartment, Dashboard, Logout, Notes, Person, PictureInPicture, ShowChart } from "@mui/icons-material";
import { Button } from "@mui/material";
import Link from "next/link";
import React from "react";
export default function Sidebar() {
return (
<>
<div className="bg-white rounded-2xl h-full py-4 space-y-10 flex flex-col items-start justify-center">
{/* <Link
href={"/dashboard"}
className="flex items-center text-[#2a5298] w-full justify-start gap-x-4 hover:bg-linear-to-r hover:from-[#1e3c72] hover:via-[#2a5298] hover:to-[#4facfe] hover:text-white px-4 py-4 hover:cursor-pointer transition-all duration-300
"
>
<span>
<Dashboard />
</span>
<span>داشبورد</span>
</Link> */}
<Link
href={"/tickets/create"}
className="flex items-center text-[#2a5298] w-full justify-start gap-x-4 hover:bg-linear-to-r hover:from-[#1e3c72] hover:via-[#2a5298] hover:to-[#4facfe] hover:text-white px-4 py-4 hover:cursor-pointer transition-all duration-300
"
>
<span>
<PictureInPicture />
</span>
<span> ثبت تيكت</span>
</Link>
<Link
href={"/tickets"}
className="flex items-center text-[#2a5298] w-full justify-start gap-x-4 hover:bg-linear-to-r hover:from-[#1e3c72] hover:via-[#2a5298] hover:to-[#4facfe] hover:text-white px-4 py-4 hover:cursor-pointer transition-all duration-300
"
>
<span>
<Notes />
</span>
<span>مشاهده تيكت ها</span>
</Link>
{/* <Link
href={"/departments"}
className="flex items-center text-[#2a5298] w-full justify-start gap-x-4 hover:bg-linear-to-r hover:from-[#1e3c72] hover:via-[#2a5298] hover:to-[#4facfe] hover:text-white px-4 py-4 hover:cursor-pointer transition-all duration-300
"
>
<span>
<Apartment />
</span>
<span>مديريت واحد ها</span>
</Link>
<Link
href={"/users"}
className="flex items-center text-[#2a5298] w-full justify-start gap-x-4 hover:bg-linear-to-r hover:from-[#1e3c72] hover:via-[#2a5298] hover:to-[#4facfe] hover:text-white px-4 py-4 hover:cursor-pointer transition-all duration-300
"
>
<span>
<Person />
</span>
<span>مديريت كاربران</span>
</Link> */}
<Link
href={"/reports"}
className="flex items-center text-[#2a5298] w-full justify-start gap-x-4 hover:bg-linear-to-r hover:from-[#1e3c72] hover:via-[#2a5298] hover:to-[#4facfe] hover:text-white px-4 py-4 hover:cursor-pointer transition-all duration-300
"
>
<span>
<ShowChart />
</span>
<span> گزارش گيري</span>
</Link>
<Button
className="flex items-center text-[#2a5298] w-full justify-start gap-x-4 hover:cursor-pointer transition-all duration-300
"
>
<span>
<Logout />
</span>
<span> خروج از حساب</span>
</Button>
</div>
</>
);
}