notification process
This commit is contained in:
@@ -13,9 +13,17 @@ const MODULES = [
|
||||
{ id: 'tenders', name: 'إدارة المناقصات', nameEn: 'Tender Management' },
|
||||
{ id: 'inventory', name: 'المخزون والأصول', nameEn: 'Inventory & Assets' },
|
||||
{ id: 'projects', name: 'المهام والمشاريع', nameEn: 'Tasks & Projects' },
|
||||
|
||||
{ id: 'hr', name: 'الموارد البشرية', nameEn: 'HR Management' },
|
||||
|
||||
{ id: 'leave_requests', name: 'طلبات الإجازات', nameEn: 'Leave Requests' },
|
||||
{ id: 'overtime_requests', name: 'طلبات الساعات الإضافية', nameEn: 'Overtime Requests' },
|
||||
{ id: 'loan_requests', name: 'طلبات القروض', nameEn: 'Loan Requests' },
|
||||
{ id: 'purchase_requests', name: 'طلبات الشراء', nameEn: 'Purchase Requests' },
|
||||
|
||||
{ id: 'department_leave_requests', name: 'طلبات إجازات القسم', nameEn: 'Department Leave Requests' },
|
||||
{ id: 'department_overtime_requests', name: 'طلبات الساعات الإضافية', nameEn: 'Department Overtime Requests' },
|
||||
{ id: 'department_overtime_requests', name: 'طلبات الساعات الإضافية للقسم', nameEn: 'Department Overtime Requests' },
|
||||
|
||||
{ id: 'portal', name: 'البوابة الذاتية', nameEn: 'My Portal' },
|
||||
{ id: 'marketing', name: 'التسويق', nameEn: 'Marketing' },
|
||||
{ id: 'admin', name: 'لوحة الإدارة', nameEn: 'Admin' },
|
||||
@@ -28,6 +36,7 @@ const ACTIONS = [
|
||||
{ id: 'delete', name: 'حذف' },
|
||||
{ id: 'export', name: 'تصدير' },
|
||||
{ id: 'approve', name: 'اعتماد' },
|
||||
{ id: 'notify', name: 'إشعار' },
|
||||
{ id: 'merge', name: 'دمج' },
|
||||
];
|
||||
|
||||
|
||||
@@ -14,9 +14,17 @@ const MODULES = [
|
||||
{ id: 'tenders', name: 'إدارة المناقصات', nameEn: 'Tender Management' },
|
||||
{ id: 'inventory', name: 'المخزون والأصول', nameEn: 'Inventory & Assets' },
|
||||
{ id: 'projects', name: 'المهام والمشاريع', nameEn: 'Tasks & Projects' },
|
||||
|
||||
{ id: 'hr', name: 'الموارد البشرية', nameEn: 'HR Management' },
|
||||
|
||||
{ id: 'leave_requests', name: 'طلبات الإجازات', nameEn: 'Leave Requests' },
|
||||
{ id: 'overtime_requests', name: 'طلبات الساعات الإضافية', nameEn: 'Overtime Requests' },
|
||||
{ id: 'loan_requests', name: 'طلبات القروض', nameEn: 'Loan Requests' },
|
||||
{ id: 'purchase_requests', name: 'طلبات الشراء', nameEn: 'Purchase Requests' },
|
||||
|
||||
{ id: 'department_leave_requests', name: 'طلبات إجازات القسم', nameEn: 'Department Leave Requests' },
|
||||
{ id: 'department_overtime_requests', name: 'طلبات الساعات الإضافية', nameEn: 'Department Overtime Requests' },
|
||||
{ id: 'department_overtime_requests', name: 'طلبات الساعات الإضافية للقسم', nameEn: 'Department Overtime Requests' },
|
||||
|
||||
{ id: 'portal', name: 'البوابة الذاتية', nameEn: 'My Portal' },
|
||||
{ id: 'marketing', name: 'التسويق', nameEn: 'Marketing' },
|
||||
{ id: 'admin', name: 'لوحة الإدارة', nameEn: 'Admin' },
|
||||
@@ -29,6 +37,7 @@ const ACTIONS = [
|
||||
{ id: 'delete', name: 'حذف' },
|
||||
{ id: 'export', name: 'تصدير' },
|
||||
{ id: 'approve', name: 'اعتماد' },
|
||||
{ id: 'notify', name: 'إشعار' },
|
||||
{ id: 'merge', name: 'دمج' },
|
||||
];
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import Image from 'next/image'
|
||||
import logoImage from '@/assets/logo.png'
|
||||
import ProtectedRoute from '@/components/ProtectedRoute'
|
||||
@@ -8,6 +8,7 @@ import { useAuth } from '@/contexts/AuthContext'
|
||||
import { useLanguage } from '@/contexts/LanguageContext'
|
||||
import LanguageSwitcher from '@/components/LanguageSwitcher'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import {
|
||||
Users,
|
||||
User,
|
||||
@@ -23,21 +24,191 @@ import {
|
||||
Shield,
|
||||
FileText
|
||||
} from 'lucide-react'
|
||||
import { dashboardAPI } from '@/lib/api'
|
||||
import { dashboardAPI, notificationsAPI } from '@/lib/api'
|
||||
import { portalAPI } from '@/lib/api/portal'
|
||||
import { hrAdminAPI } from '@/lib/api/hrAdmin'
|
||||
|
||||
function DashboardContent() {
|
||||
const { user, logout, hasPermission } = useAuth()
|
||||
const { user, logout, hasPermission } = useAuth()
|
||||
const router = useRouter()
|
||||
const { t, language, dir } = useLanguage()
|
||||
const [stats, setStats] = useState({ contacts: 0, activeTasks: 0, notifications: 0 })
|
||||
|
||||
const [stats, setStats] = useState({
|
||||
contacts: 0,
|
||||
activeTasks: 0,
|
||||
notifications: 0,
|
||||
})
|
||||
|
||||
const [showNotifications, setShowNotifications] = useState(false)
|
||||
const [notifications, setNotifications] = useState<any[]>([])
|
||||
const [notificationsLoading, setNotificationsLoading] = useState(false)
|
||||
const notificationsRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const [pendingApprovals, setPendingApprovals] = useState({
|
||||
managedLeaves: 0,
|
||||
managedOvertime: 0,
|
||||
purchaseRequests: 0,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
dashboardAPI.getStats()
|
||||
dashboardAPI
|
||||
.getStats()
|
||||
.then((res) => {
|
||||
if (res.data?.data) setStats(res.data.data)
|
||||
})
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
const loadNotifications = async () => {
|
||||
setNotificationsLoading(true)
|
||||
try {
|
||||
const res = await notificationsAPI.getMy({ page: 1, pageSize: 10 })
|
||||
const items = res.data?.data?.notifications || []
|
||||
setNotifications(items)
|
||||
} catch {
|
||||
setNotifications([])
|
||||
} finally {
|
||||
setNotificationsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const markNotificationAsRead = async (id: string) => {
|
||||
try {
|
||||
await notificationsAPI.markAsRead(id)
|
||||
|
||||
setNotifications((prev) =>
|
||||
prev.map((item) =>
|
||||
item.id === id
|
||||
? { ...item, isRead: true, readAt: new Date().toISOString() }
|
||||
: item
|
||||
)
|
||||
)
|
||||
|
||||
setStats((prev) => ({
|
||||
...prev,
|
||||
notifications: Math.max(0, prev.notifications - 1),
|
||||
}))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const resolveNotificationUrl = (notification: any) => {
|
||||
if (notification.entityType === 'LEAVE') {
|
||||
if (notification.type === 'LEAVE_REQUEST_SUBMITTED') {
|
||||
return '/portal/managed-leaves'
|
||||
}
|
||||
return '/portal/leave'
|
||||
}
|
||||
|
||||
if (notification.entityType === 'OVERTIME_REQUEST') {
|
||||
if (notification.type === 'OVERTIME_REQUEST_SUBMITTED') {
|
||||
return '/portal/managed-overtime-requests'
|
||||
}
|
||||
return '/portal/overtime'
|
||||
}
|
||||
|
||||
if (notification.entityType === 'PURCHASE_REQUEST') {
|
||||
if (notification.type === 'PURCHASE_REQUEST_SUBMITTED') {
|
||||
return '/hr?tab=purchases'
|
||||
}
|
||||
return '/portal/purchase-requests'
|
||||
}
|
||||
|
||||
if (notification.entityType === 'LOAN') {
|
||||
if (
|
||||
notification.type === 'LOAN_REQUEST_SUBMITTED' ||
|
||||
notification.type === 'LOAN_REQUEST_PENDING_ADMIN'
|
||||
) {
|
||||
return '/hr?tab=loans'
|
||||
}
|
||||
return '/portal/loans'
|
||||
}
|
||||
|
||||
if (notification.type === 'TENDER_DIRECTIVE_ASSIGNED') {
|
||||
if (notification.entityType === 'TENDER' && notification.entityId) {
|
||||
return `/tenders/${notification.entityId}?tab=directives`
|
||||
}
|
||||
|
||||
return '/tenders'
|
||||
}
|
||||
|
||||
return '/dashboard'
|
||||
}
|
||||
|
||||
const handleNotificationClick = async (notification: any) => {
|
||||
if (!notification.isRead) {
|
||||
await markNotificationAsRead(notification.id)
|
||||
}
|
||||
|
||||
const targetUrl = resolveNotificationUrl(notification)
|
||||
setShowNotifications(false)
|
||||
router.push(targetUrl)
|
||||
}
|
||||
|
||||
const handleToggleNotifications = async () => {
|
||||
const next = !showNotifications
|
||||
setShowNotifications(next)
|
||||
|
||||
if (next) {
|
||||
await loadNotifications()
|
||||
}
|
||||
}
|
||||
|
||||
const loadPendingApprovals = async () => {
|
||||
try {
|
||||
const [managedLeaves, managedOvertime, purchaseRequests] = await Promise.all([
|
||||
canViewManagedLeaves
|
||||
? portalAPI.getManagedLeaves('PENDING')
|
||||
: Promise.resolve([]),
|
||||
canViewManagedOvertime
|
||||
? portalAPI.getManagedOvertimeRequests()
|
||||
: Promise.resolve([]),
|
||||
canApproveHr
|
||||
? hrAdminAPI
|
||||
.getPurchaseRequests({ status: 'PENDING', page: 1, pageSize: 50 })
|
||||
.then((r) => r.purchaseRequests || [])
|
||||
: Promise.resolve([]),
|
||||
])
|
||||
|
||||
const total =
|
||||
managedLeaves.length +
|
||||
managedOvertime.length +
|
||||
purchaseRequests.length
|
||||
|
||||
setPendingApprovals({
|
||||
managedLeaves: managedLeaves.length,
|
||||
managedOvertime: managedOvertime.length,
|
||||
purchaseRequests: purchaseRequests.length,
|
||||
total,
|
||||
})
|
||||
} catch {
|
||||
setPendingApprovals({
|
||||
managedLeaves: 0,
|
||||
managedOvertime: 0,
|
||||
purchaseRequests: 0,
|
||||
total: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
notificationsRef.current &&
|
||||
!notificationsRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setShowNotifications(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
const allModules = [
|
||||
{
|
||||
id: 'contacts',
|
||||
@@ -135,6 +306,9 @@ function DashboardContent() {
|
||||
const availableModules = allModules.filter(module =>
|
||||
hasPermission(module.permission, 'view')
|
||||
)
|
||||
const canViewManagedLeaves = hasPermission('department_leave_requests', 'view')
|
||||
const canViewManagedOvertime = hasPermission('department_overtime_requests', 'view')
|
||||
const canApproveHr = hasPermission('hr', 'approve')
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
|
||||
@@ -182,12 +356,86 @@ function DashboardContent() {
|
||||
)}
|
||||
|
||||
{/* Notifications */}
|
||||
<button className="p-2 hover:bg-gray-100 rounded-lg transition-colors relative">
|
||||
<Bell className="h-5 w-5 text-gray-600" />
|
||||
{stats.notifications > 0 && (
|
||||
<span className="absolute top-1 right-1 h-2 w-2 bg-red-500 rounded-full"></span>
|
||||
<div className="relative" ref={notificationsRef}>
|
||||
<button
|
||||
onClick={handleToggleNotifications}
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors relative"
|
||||
title="الإشعارات"
|
||||
>
|
||||
<Bell className="h-5 w-5 text-gray-600" />
|
||||
{stats.notifications > 0 && (
|
||||
<span className="absolute top-1 right-1 h-2 w-2 bg-red-500 rounded-full"></span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{showNotifications && (
|
||||
<div className="absolute left-0 mt-2 w-96 bg-white border border-gray-200 rounded-xl shadow-xl z-50 overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-gray-100 flex items-center justify-between">
|
||||
<h3 className="text-sm font-bold text-gray-900">الإشعارات</h3>
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
await notificationsAPI.markAllAsRead()
|
||||
setNotifications((prev) =>
|
||||
prev.map((item) => ({
|
||||
...item,
|
||||
isRead: true,
|
||||
readAt: new Date().toISOString(),
|
||||
}))
|
||||
)
|
||||
setStats((prev) => ({ ...prev, notifications: 0 }))
|
||||
} catch {}
|
||||
}}
|
||||
className="text-xs text-blue-600 hover:text-blue-700"
|
||||
>
|
||||
تعليم الكل كمقروء
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
{notificationsLoading ? (
|
||||
<div className="p-4 text-sm text-gray-500 text-center">
|
||||
جاري تحميل الإشعارات...
|
||||
</div>
|
||||
) : notifications.length === 0 ? (
|
||||
<div className="p-4 text-sm text-gray-500 text-center">
|
||||
لا توجد إشعارات
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-100">
|
||||
{notifications.map((notification) => (
|
||||
<button
|
||||
key={notification.id}
|
||||
onClick={() => handleNotificationClick(notification)}
|
||||
className={`w-full text-right px-4 py-3 hover:bg-gray-50 transition-colors ${
|
||||
notification.isRead ? 'bg-white' : 'bg-blue-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-semibold text-gray-900">
|
||||
{notification.title}
|
||||
</p>
|
||||
<p className="text-xs text-gray-600 mt-1">
|
||||
{notification.message}
|
||||
</p>
|
||||
<p className="text-[11px] text-gray-400 mt-2">
|
||||
{new Date(notification.createdAt).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!notification.isRead && (
|
||||
<span className="mt-1 h-2.5 w-2.5 rounded-full bg-blue-500 flex-shrink-0"></span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Settings */}
|
||||
<button className="p-2 hover:bg-gray-100 rounded-lg transition-colors">
|
||||
@@ -268,6 +516,65 @@ function DashboardContent() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pending Approvals */}
|
||||
{pendingApprovals.total > 0 && (
|
||||
<div className="bg-white rounded-2xl shadow-md border border-gray-100 p-6 mb-8">
|
||||
<div className="flex items-center justify-between mb-5">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-gray-900">بانتظار موافقتك</h3>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
كل الطلبات التي تحتاج قرارك الآن
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-amber-100 text-amber-700 px-4 py-2 rounded-lg font-bold">
|
||||
{pendingApprovals.total}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{canViewManagedLeaves && (
|
||||
<button
|
||||
onClick={() => router.push('/portal/managed-leaves')}
|
||||
className="text-right border border-gray-200 rounded-xl p-4 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<p className="text-sm text-gray-500">طلبات إجازات القسم</p>
|
||||
<p className="text-2xl font-bold text-gray-900 mt-2">
|
||||
{pendingApprovals.managedLeaves}
|
||||
</p>
|
||||
<p className="text-xs text-blue-600 mt-2">فتح الصفحة</p>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{canViewManagedOvertime && (
|
||||
<button
|
||||
onClick={() => router.push('/portal/managed-overtime-requests')}
|
||||
className="text-right border border-gray-200 rounded-xl p-4 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<p className="text-sm text-gray-500">طلبات الساعات الإضافية</p>
|
||||
<p className="text-2xl font-bold text-gray-900 mt-2">
|
||||
{pendingApprovals.managedOvertime}
|
||||
</p>
|
||||
<p className="text-xs text-blue-600 mt-2">فتح الصفحة</p>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{canApproveHr && (
|
||||
<button
|
||||
onClick={() => router.push('/hr?tab=purchases')}
|
||||
className="text-right border border-gray-200 rounded-xl p-4 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<p className="text-sm text-gray-500">طلبات الشراء</p>
|
||||
<p className="text-2xl font-bold text-gray-900 mt-2">
|
||||
{pendingApprovals.purchaseRequests}
|
||||
</p>
|
||||
<p className="text-xs text-blue-600 mt-2">فتح الصفحة</p>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Available Modules */}
|
||||
<div className="mb-8">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-6">الوحدات المتاحة</h3>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import ProtectedRoute from '@/components/ProtectedRoute'
|
||||
import Modal from '@/components/Modal'
|
||||
import LoadingSpinner from '@/components/LoadingSpinner'
|
||||
@@ -245,6 +246,8 @@ function EmployeeFormFields({
|
||||
|
||||
function HRContent() {
|
||||
// State Management
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const [employees, setEmployees] = useState<Employee[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@@ -296,7 +299,13 @@ function HRContent() {
|
||||
const [loadingDepts, setLoadingDepts] = useState(false)
|
||||
|
||||
// Tabs: employees | departments | orgchart | leaves | loans | purchases | contracts
|
||||
const [activeTab, setActiveTab] = useState<'employees' | 'departments' | 'orgchart' | 'leaves' | 'loans' | 'purchases' | 'contracts'>('employees')
|
||||
type HRTab = 'employees' | 'departments' | 'orgchart' | 'leaves' | 'loans' | 'purchases' | 'contracts'
|
||||
|
||||
const [activeTab, setActiveTab] = useState<HRTab>('employees')
|
||||
const openTab = (tab: HRTab) => {
|
||||
setActiveTab(tab)
|
||||
router.replace(`/hr?tab=${tab}`)
|
||||
}
|
||||
const [hierarchy, setHierarchy] = useState<Department[]>([])
|
||||
const [loadingHierarchy, setLoadingHierarchy] = useState(false)
|
||||
|
||||
@@ -391,6 +400,24 @@ function HRContent() {
|
||||
}
|
||||
}, [activeTab])
|
||||
|
||||
useEffect(() => {
|
||||
const tabParam = searchParams.get('tab') as HRTab | null
|
||||
|
||||
const allowedTabs: HRTab[] = [
|
||||
'employees',
|
||||
'departments',
|
||||
'orgchart',
|
||||
'leaves',
|
||||
'loans',
|
||||
'purchases',
|
||||
'contracts',
|
||||
]
|
||||
|
||||
if (tabParam && allowedTabs.includes(tabParam)) {
|
||||
setActiveTab(tabParam)
|
||||
}
|
||||
}, [searchParams])
|
||||
|
||||
// Fetch Employees (with debouncing for search)
|
||||
const fetchEmployees = useCallback(async () => {
|
||||
setLoading(true)
|
||||
@@ -721,7 +748,7 @@ function HRContent() {
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<nav className="flex gap-4">
|
||||
<button
|
||||
onClick={() => setActiveTab('employees')}
|
||||
onClick={() => openTab('employees')}
|
||||
className={`py-4 px-2 border-b-2 font-medium text-sm transition-colors ${
|
||||
activeTab === 'employees'
|
||||
? 'border-red-600 text-red-600'
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useParams} from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import {
|
||||
@@ -35,6 +36,7 @@ const DIRECTIVE_TYPE_LABELS: Record<string, string> = {
|
||||
}
|
||||
|
||||
function TenderDetailContent() {
|
||||
const searchParams = useSearchParams()
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
const tenderId = params.id as string
|
||||
@@ -43,7 +45,12 @@ function TenderDetailContent() {
|
||||
const [tender, setTender] = useState<Tender | null>(null)
|
||||
const [history, setHistory] = useState<any[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [activeTab, setActiveTab] = useState<'info' | 'directives' | 'attachments' | 'history'>('info')
|
||||
type TenderTab = 'details' | 'directives' | 'attachments' | 'logs' | 'info' |'history'
|
||||
const [activeTab, setActiveTab] = useState<TenderTab>('details')
|
||||
const openTab = (tab: TenderTab) => {
|
||||
setActiveTab(tab)
|
||||
router.replace(`/tenders/${params.id}?tab=${tab}`)
|
||||
}
|
||||
const [showDirectiveModal, setShowDirectiveModal] = useState(false)
|
||||
const [showConvertModal, setShowConvertModal] = useState(false)
|
||||
const [showCompleteModal, setShowCompleteModal] = useState<TenderDirective | null>(null)
|
||||
@@ -85,6 +92,17 @@ function TenderDetailContent() {
|
||||
setHistory(data)
|
||||
} catch {}
|
||||
}
|
||||
useEffect(() => {
|
||||
const tabParam = searchParams.get('tab') as TenderTab | null
|
||||
|
||||
const allowedTabs: TenderTab[] = ['details', 'directives', 'attachments', 'logs']
|
||||
|
||||
if (tabParam && allowedTabs.includes(tabParam)) {
|
||||
setActiveTab(tabParam)
|
||||
}
|
||||
}, [searchParams])
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
fetchTender()
|
||||
|
||||
@@ -81,6 +81,21 @@ export const dashboardAPI = {
|
||||
getStats: () => api.get<{ success: boolean; data: { contacts: number; activeTasks: number; notifications: number } }>('/dashboard/stats'),
|
||||
}
|
||||
|
||||
|
||||
export const notificationsAPI = {
|
||||
getMy: (params?: { page?: number; pageSize?: number }) =>
|
||||
api.get('/notifications/my', { params }),
|
||||
|
||||
getUnreadCount: () =>
|
||||
api.get('/notifications/unread-count'),
|
||||
|
||||
markAsRead: (id: string) =>
|
||||
api.patch(`/notifications/${id}/read`),
|
||||
|
||||
markAllAsRead: () =>
|
||||
api.patch('/notifications/read-all'),
|
||||
}
|
||||
|
||||
export const crmAPI = {
|
||||
// Deals
|
||||
getDeals: (params?: any) => api.get('/crm/deals', { params }),
|
||||
|
||||
Reference in New Issue
Block a user