add dashboard tabs component
This commit is contained in:
@@ -433,7 +433,7 @@ export default function DashboardPage() {
|
|||||||
<User size={32} strokeWidth={1.5} />
|
<User size={32} strokeWidth={1.5} />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-lg font-bold text-gray-800 mb-1">{userInfo.fullName}</div>
|
<div className="text-lg font-bold text-gray-800 mb-1">{userInfo.fullName}</div>
|
||||||
<div className="text-sm text-gray-500 mb-4 font-mono" dir="ltr">{userInfo.phone}</div>
|
<div className="text-sm text-gray-500 mb-4 " dir="ltr">{userInfo.phone}</div>
|
||||||
<div className="w-full flex flex-col items-center">
|
<div className="w-full flex flex-col items-center">
|
||||||
<div className="inline-flex items-center gap-1.5 px-3 py-1 bg-yellow-100 text-yellow-700 rounded-full text-xs font-bold mb-3">
|
<div className="inline-flex items-center gap-1.5 px-3 py-1 bg-yellow-100 text-yellow-700 rounded-full text-xs font-bold mb-3">
|
||||||
<Crown size={14} /> حساب فعال
|
<Crown size={14} /> حساب فعال
|
||||||
|
|||||||
33
components/dashboard/address.tsx
Normal file
33
components/dashboard/address.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { User , Headphones } from "lucide-react";
|
||||||
|
|
||||||
|
const AddressTab = ({ addresses }:any) => {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
{addresses.map((address:any) => (
|
||||||
|
<div
|
||||||
|
key={address.id}
|
||||||
|
className={`bg-white p-6 rounded-2xl border-2 ${
|
||||||
|
address.isDefault ? 'border-[#ffd230]' : 'border-gray-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="text-gray-800 leading-relaxed font-medium mb-4 text-sm sm:text-base">
|
||||||
|
{address.text}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 text-sm text-gray-500 mb-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<User size={16} />
|
||||||
|
گیرنده: {address.receiver}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Headphones size={16} />
|
||||||
|
تماس: {address.phone}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddressTab;
|
||||||
40
components/dashboard/dash.tsx
Normal file
40
components/dashboard/dash.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import DashboardStat from "./dashboardstat";
|
||||||
|
import { Wallet, ShoppingBag, Headphones, TrendingUp } from "lucide-react";
|
||||||
|
|
||||||
|
|
||||||
|
const DashboardHomeTab = ({ ticketStatValue }: any) => {
|
||||||
|
return (
|
||||||
|
<div className="animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-800 mb-6">خوش آمدید</h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4 mb-6">
|
||||||
|
<DashboardStat
|
||||||
|
icon={<Wallet size={24} />}
|
||||||
|
color="bg-blue-50 text-blue-600"
|
||||||
|
label="موجودی کیف پول"
|
||||||
|
value="1,500,000 تومان"
|
||||||
|
/>
|
||||||
|
<DashboardStat
|
||||||
|
icon={<ShoppingBag size={24} />}
|
||||||
|
color="bg-green-50 text-green-600"
|
||||||
|
label="سفارشات جاری"
|
||||||
|
value="2 مورد"
|
||||||
|
/>
|
||||||
|
<DashboardStat
|
||||||
|
icon={<Headphones size={24} />}
|
||||||
|
color="bg-orange-50 text-orange-600"
|
||||||
|
label="تیکتها"
|
||||||
|
value={ticketStatValue}
|
||||||
|
/>
|
||||||
|
<DashboardStat
|
||||||
|
icon={<TrendingUp size={24} />}
|
||||||
|
color="bg-purple-50 text-purple-600"
|
||||||
|
label="امتیاز وفاداری"
|
||||||
|
value="2,340 امتیاز"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DashboardHomeTab;
|
||||||
11
components/dashboard/dashboardstat.tsx
Normal file
11
components/dashboard/dashboardstat.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export default function DashboardStat({ icon, color, label, value }: { icon: React.ReactNode; color: string; label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-5 rounded-2xl border border-gray-100 shadow-sm flex flex-col items-center gap-4 hover:shadow-md transition-shadow">
|
||||||
|
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${color}`}>{icon}</div>
|
||||||
|
<div className='flex flex-col items-center'>
|
||||||
|
<h4 className="text-xs text-gray-500 mb-1">{label}</h4>
|
||||||
|
<strong className="text-lg text-gray-800">{value}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
28
components/dashboard/overview.tsx
Normal file
28
components/dashboard/overview.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
const OrdersTab = ({ orders }:any) => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{orders.map((order:any) => (
|
||||||
|
<div key={order.id} className="bg-white rounded-2xl border border-gray-100 shadow-sm">
|
||||||
|
<div className="p-6 flex items-center justify-between border-b border-gray-100">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-bold text-gray-800">{order.id}</h3>
|
||||||
|
<p className="text-sm text-gray-500 mt-1">
|
||||||
|
ثبت سفارش: {order.regDate}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className={`px-3 py-1 rounded-full text-xs font-bold ${
|
||||||
|
order.statusColor === 'amber'
|
||||||
|
? 'bg-amber-100 text-amber-700'
|
||||||
|
: 'bg-green-100 text-green-700'
|
||||||
|
}`}>
|
||||||
|
{order.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrdersTab;
|
||||||
15
components/dashboard/profilefield.tsx
Normal file
15
components/dashboard/profilefield.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface ProfileFieldProps {
|
||||||
|
label: string;
|
||||||
|
value: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProfileField({ label, value }: ProfileFieldProps) {
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-50 p-4 rounded-xl border border-gray-100">
|
||||||
|
<span className="text-sm text-gray-500 block mb-1">{label}</span>
|
||||||
|
<span className="font-medium text-gray-800 block">{value || '-'}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
components/dashboard/profileview.tsx
Normal file
20
components/dashboard/profileview.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import ProfileField from "./profilefield";
|
||||||
|
|
||||||
|
const ProfileTab = ({ userInfo }:any) => {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6">
|
||||||
|
<h2 className="text-xl font-bold text-gray-800 mb-6 border-b border-gray-100 pb-4">
|
||||||
|
اطلاعات شخصی
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||||
|
<ProfileField label="نام و نام خانوادگی" value={userInfo.fullName} />
|
||||||
|
<ProfileField label="شماره موبایل" value={userInfo.phone} />
|
||||||
|
<ProfileField label="نام کاربری" value={userInfo.username} />
|
||||||
|
<ProfileField label="نقش" value={userInfo.role} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfileTab;
|
||||||
229
components/dashboard/tickets.tsx
Normal file
229
components/dashboard/tickets.tsx
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Plus } from 'lucide-react'; // فرض بر این است که از lucide-react استفاده میکنید
|
||||||
|
|
||||||
|
// TODO: کامپوننتهای کمکی و تایپهای زیر باید از فایلهای مربوطهشان ایمپورت شوند
|
||||||
|
// import TicketSummaryCard from './TicketSummaryCard';
|
||||||
|
// import FilterButton from './FilterButton';
|
||||||
|
// import LoadingCard from './LoadingCard';
|
||||||
|
// import ErrorCard from './ErrorCard';
|
||||||
|
// import EmptyCard from './EmptyCard';
|
||||||
|
// import StatusBadge from './StatusBadge';
|
||||||
|
// import PriorityBadge from './PriorityBadge';
|
||||||
|
// import Pager from './Pager';
|
||||||
|
// import TicketConversationCard from './TicketConversationCard';
|
||||||
|
// import { formatDateTime } from '../utils/dateHelpers';
|
||||||
|
|
||||||
|
import {
|
||||||
|
closeUserTicket,
|
||||||
|
createUserTicket,
|
||||||
|
getAdminTicketById,
|
||||||
|
getAdminTickets,
|
||||||
|
getUserTicketById,
|
||||||
|
getUserTickets,
|
||||||
|
sendAdminTicketMessage,
|
||||||
|
sendUserTicketMessage,
|
||||||
|
TicketDetail,
|
||||||
|
TicketListItem,
|
||||||
|
TicketPriority,
|
||||||
|
TicketStatus,
|
||||||
|
updateAdminTicket,
|
||||||
|
} from "@/public/src/services/tickets/api";
|
||||||
|
|
||||||
|
import { LoaderCircle,Send , Headphones } from 'lucide-react';
|
||||||
|
|
||||||
|
const statusLabels: Record<TicketStatus, string> = {
|
||||||
|
waiting_for_admin: "در انتظار پاسخ پشتیبانی",
|
||||||
|
waiting_for_user: "در انتظار پاسخ کاربر",
|
||||||
|
closed: "بسته شده",
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const priorityLabels: Record<TicketPriority, string> = {
|
||||||
|
low: "کم",
|
||||||
|
normal: "عادی",
|
||||||
|
high: "زیاد",
|
||||||
|
urgent: "فوری",
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function TicketConversationCard({ ticket, reply, onReplyChange, onReplySubmit, replyLoading, onCloseTicket, adminMode = false }: {
|
||||||
|
ticket: TicketDetail;
|
||||||
|
reply: string;
|
||||||
|
onReplyChange: (value: string) => void;
|
||||||
|
onReplySubmit: () => void;
|
||||||
|
replyLoading: boolean;
|
||||||
|
onCloseTicket?: () => void;
|
||||||
|
adminMode?: boolean;
|
||||||
|
}) {
|
||||||
|
const closed = ticket.status === "closed";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-6 rounded-2xl border border-gray-100 shadow-sm">
|
||||||
|
<div className="flex items-center justify-between gap-4 mb-6">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-gray-400 mb-2">{ticket.ticketNumber}</div>
|
||||||
|
<h3 className="text-lg font-bold text-gray-800">{ticket.subject}</h3>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<StatusBadge status={ticket.status} />
|
||||||
|
<PriorityBadge priority={ticket.priority} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!adminMode && onCloseTicket && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<button onClick={onCloseTicket} className="px-4 py-2 rounded-xl text-sm font-medium border border-red-200 text-red-500 hover:bg-red-50">بستن تیکت</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-4 max-h-[460px] overflow-y-auto mb-6">
|
||||||
|
{ticket.messages.map((message) => (
|
||||||
|
<div key={message.id} className={`flex ${message.senderType === 'user' ? 'justify-end' : 'justify-start'}`}>
|
||||||
|
<div className={`max-w-[85%] rounded-2xl p-4 ${message.senderType === 'user' ? 'bg-[#1a2332] text-white' : 'bg-gray-50 text-gray-800 border border-gray-100'}`}>
|
||||||
|
<div className={`text-xs mb-2 ${message.senderType === 'user' ? 'text-gray-300' : 'text-gray-500'}`}>{message.sender.fullName} • {message.senderType === 'user' ? 'کاربر' : 'پشتیبانی'}</div>
|
||||||
|
<div className="text-sm leading-7">{message.message}</div>
|
||||||
|
<div className={`text-[11px] mt-3 ${message.senderType === 'user' ? 'text-gray-300' : 'text-gray-400'}`}>{formatDateTime(message.createdAt)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-gray-100 pt-4">
|
||||||
|
<textarea value={reply} onChange={(e) => onReplyChange(e.target.value)} disabled={closed} rows={4} placeholder={closed ? "این تیکت بسته شده و ارسال پیام غیرفعال است." : adminMode ? "پاسخ پشتیبانی را بنویسید..." : "پیام خود را بنویسید..."} className="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm outline-none focus:border-[#ffb900] resize-none disabled:bg-gray-50 disabled:text-gray-400" />
|
||||||
|
<div className="flex items-center justify-between mt-3 gap-4">
|
||||||
|
<div className="text-xs text-gray-500">{closed ? "برای ادامه گفتگو باید تیکت جدید ثبت شود." : "آخرین بروزرسانی: " + formatDateTime(ticket.updatedAt)}</div>
|
||||||
|
<button onClick={onReplySubmit} disabled={closed || replyLoading || !reply.trim()} className="bg-[#1a2332] text-white px-5 py-2.5 rounded-xl text-sm font-medium hover:bg-[#ffb900] disabled:opacity-60 flex items-center gap-2">
|
||||||
|
{replyLoading ? <LoaderCircle size={16} className="animate-spin" /> : <Send size={16} />}
|
||||||
|
ارسال پیام
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function TicketSummaryCard({ value, label, color }: { value: string; label: string; color: string }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-5 rounded-2xl border border-gray-100 shadow-sm flex flex-col items-center justify-center">
|
||||||
|
<span className={`text-3xl font-bold mb-2 ${color}`}>{value}</span>
|
||||||
|
<div className="text-sm text-gray-500">{label}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function FilterButton({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }) {
|
||||||
|
return <button onClick={onClick} className={`${active ? 'bg-gray-800 text-white' : 'bg-white border border-gray-200 text-gray-600'} px-5 py-2 rounded-full text-sm font-medium transition-colors`}>{children}</button>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Pager({ page, totalPages, onPrev, onNext }: { page: number; totalPages: number; onPrev: () => void; onNext: () => void }) {
|
||||||
|
return <div className="flex items-center justify-between pt-2"><button onClick={onPrev} disabled={page <= 1} className="px-4 py-2 rounded-xl border border-gray-200 text-sm text-gray-600 disabled:opacity-50">قبلی</button><span className="text-xs text-gray-500">صفحه {page.toLocaleString('fa-IR')} از {totalPages.toLocaleString('fa-IR')}</span><button onClick={onNext} disabled={page >= totalPages} className="px-4 py-2 rounded-xl border border-gray-200 text-sm text-gray-600 disabled:opacity-50">بعدی</button></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LoadingCard({ text }: { text: string }) {
|
||||||
|
return <div className="bg-white p-8 rounded-2xl border border-gray-100 shadow-sm text-center text-gray-500 flex items-center justify-center gap-2"><LoaderCircle size={18} className="animate-spin" />{text}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ErrorCard({ text }: { text: string }) {
|
||||||
|
return <div className="bg-red-50 text-red-600 p-5 rounded-2xl border border-red-100 text-sm">{text}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmptyCard({ title, description }: { title: string; description: string }) {
|
||||||
|
return <div className="bg-white p-8 rounded-2xl border border-gray-100 shadow-sm text-center"><div className="w-14 h-14 mx-auto rounded-full bg-gray-100 flex items-center justify-center text-gray-400 mb-4"><Headphones size={24} /></div><h3 className="text-lg font-bold text-gray-800 mb-2">{title}</h3><p className="text-sm text-gray-500 leading-7">{description}</p></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusBadge({ status }: { status: TicketStatus }) {
|
||||||
|
return <span className={`px-3 py-1 rounded-full text-xs font-medium ${status === "waiting_for_admin" ? 'bg-amber-100 text-amber-800' : status === "waiting_for_user" ? 'bg-blue-100 text-blue-700' : 'bg-gray-200 text-gray-700'}`}>{statusLabels[status]}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function PriorityBadge({ priority }: { priority: TicketPriority }) {
|
||||||
|
return <span className={`px-2.5 py-1 rounded-full text-[11px] font-medium ${priority === "low" ? 'bg-emerald-100 text-emerald-700' : priority === "normal" ? 'bg-gray-100 text-gray-700' : priority === "high" ? 'bg-orange-100 text-orange-700' : 'bg-red-100 text-red-700'}`}>{priorityLabels[priority]}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function formatDateTime(value?: string | null) {
|
||||||
|
if (!value) return "ثبت نشده";
|
||||||
|
return new Intl.DateTimeFormat("fa-IR", { dateStyle: "medium", timeStyle: "short" }).format(new Date(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function TicketsTab(props: {
|
||||||
|
|
||||||
|
tickets: any[]; // به جای any میتوانید TicketListItem قرار دهید
|
||||||
|
loading: boolean;
|
||||||
|
error: string;
|
||||||
|
counts: any; // TicketCounts
|
||||||
|
selectedTicketId: string;
|
||||||
|
selectedTicket: any | null; // TicketDetail
|
||||||
|
detailLoading: boolean;
|
||||||
|
reply: string;
|
||||||
|
replyLoading: boolean;
|
||||||
|
statusFilter: "" | string; // TicketStatus
|
||||||
|
onSelectTicket: (id: string) => void;
|
||||||
|
onStatusFilterChange: (status: "" | string) => void;
|
||||||
|
onReplyChange: (value: string) => void;
|
||||||
|
onReplySubmit: () => void;
|
||||||
|
onCloseTicket: () => void;
|
||||||
|
onOpenCreateTicket: () => void;
|
||||||
|
page: number;
|
||||||
|
totalPages: number;
|
||||||
|
onPrevPage: () => void;
|
||||||
|
onNextPage: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="animate-in fade-in duration-500">
|
||||||
|
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-800 m-0">تیکتهای پشتیبانی</h1>
|
||||||
|
<button onClick={props.onOpenCreateTicket} className="bg-[#1a2332] hover:bg-[#ffb900] text-white px-5 py-2.5 rounded-xl flex items-center gap-2 transition-colors text-sm font-medium">
|
||||||
|
<Plus size={18} /> تیکت جدید
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||||
|
<TicketSummaryCard value={props.counts.total.toLocaleString('fa-IR')} label="کل تیکتها" color="text-gray-800" />
|
||||||
|
<TicketSummaryCard value={props.counts.waiting_for_admin.toLocaleString('fa-IR')} label="در انتظار پشتیبانی" color="text-amber-600" />
|
||||||
|
<TicketSummaryCard value={props.counts.waiting_for_user.toLocaleString('fa-IR')} label="در انتظار شما" color="text-blue-700" />
|
||||||
|
<TicketSummaryCard value={props.counts.closed.toLocaleString('fa-IR')} label="بسته شده" color="text-gray-700" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-6 overflow-x-auto pb-2">
|
||||||
|
<div className="flex gap-2 min-w-max">
|
||||||
|
<FilterButton active={props.statusFilter === ""} onClick={() => props.onStatusFilterChange("")}>همه تیکتها</FilterButton>
|
||||||
|
<FilterButton active={props.statusFilter === "waiting_for_admin"} onClick={() => props.onStatusFilterChange("waiting_for_admin")}>در انتظار پشتیبانی</FilterButton>
|
||||||
|
<FilterButton active={props.statusFilter === "waiting_for_user"} onClick={() => props.onStatusFilterChange("waiting_for_user")}>در انتظار شما</FilterButton>
|
||||||
|
<FilterButton active={props.statusFilter === "closed"} onClick={() => props.onStatusFilterChange("closed")}>بسته شده</FilterButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 xl:grid-cols-[420px_1fr] gap-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{props.loading ? <LoadingCard text="در حال دریافت تیکتها..." /> : props.error ? <ErrorCard text={props.error} /> : props.tickets.length === 0 ? <EmptyCard title="تیکتی پیدا نشد" description="با فیلتر فعلی تیکتی وجود ندارد." /> : (
|
||||||
|
<>
|
||||||
|
{props.tickets.map((ticket) => (
|
||||||
|
<button key={ticket.id} onClick={() => props.onSelectTicket(ticket.id)} className={`w-full text-right bg-white p-5 rounded-2xl border shadow-sm transition ${props.selectedTicketId === ticket.id ? 'border-[#ffb900] shadow-md' : 'border-gray-100 hover:shadow-md'}`}>
|
||||||
|
<div className="flex items-start justify-between gap-3 mb-3">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-base font-bold text-gray-800">{ticket.subject}</h3>
|
||||||
|
<div className="text-xs text-gray-400 mt-2">{ticket.ticketNumber}</div>
|
||||||
|
</div>
|
||||||
|
<StatusBadge status={ticket.status} />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap items-center gap-2 text-xs text-gray-500">
|
||||||
|
<PriorityBadge priority={ticket.priority} />
|
||||||
|
<span>{formatDateTime(ticket.lastMessageAt || ticket.createdAt)}</span>
|
||||||
|
<span>{ticket.messagesCount} پیام</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 text-sm text-gray-600 line-clamp-2">{ticket.lastMessage?.message || "بدون پیام"}</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<Pager page={props.page} totalPages={props.totalPages} onPrev={props.onPrevPage} onNext={props.onNextPage} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{props.detailLoading ? <LoadingCard text="در حال دریافت جزئیات تیکت..." /> : !props.selectedTicket ? <EmptyCard title="یک تیکت را انتخاب کنید" description="برای دیدن جزئیات گفتگو، یکی از تیکتها را انتخاب کنید." /> : (
|
||||||
|
<TicketConversationCard ticket={props.selectedTicket} reply={props.reply} onReplyChange={props.onReplyChange} onReplySubmit={props.onReplySubmit} replyLoading={props.replyLoading} onCloseTicket={props.selectedTicket.status === "closed" ? undefined : props.onCloseTicket} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
components/dashboard/wallet.tsx
Normal file
16
components/dashboard/wallet.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
const WalletTab = ({ balance }:any) => {
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-800 text-white p-6 sm:p-8 rounded-2xl mb-8">
|
||||||
|
<span className="text-gray-300 text-sm mb-2 block">
|
||||||
|
موجودی فعلی حساب شما:
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="text-3xl sm:text-4xl font-bold text-yellow-400">
|
||||||
|
{balance}
|
||||||
|
<span className="text-lg font-normal text-white"> تومان</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WalletTab;
|
||||||
Reference in New Issue
Block a user