Добавлены новые зависимости, обновлены стили и улучшена структура проекта. Обновлен README с описанием функционала и технологий. Реализована анимация и адаптивный дизайн. Настроена авторизация с использованием Apollo Client.
This commit is contained in:
326
src/components/market/market-counterparties.tsx
Normal file
326
src/components/market/market-counterparties.tsx
Normal file
@ -0,0 +1,326 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useMutation } from '@apollo/client'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import {
|
||||
Users,
|
||||
Clock,
|
||||
Send,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
ArrowUpCircle,
|
||||
ArrowDownCircle
|
||||
} from 'lucide-react'
|
||||
import { OrganizationCard } from './organization-card'
|
||||
import { GET_MY_COUNTERPARTIES, GET_INCOMING_REQUESTS, GET_OUTGOING_REQUESTS } from '@/graphql/queries'
|
||||
import { RESPOND_TO_COUNTERPARTY_REQUEST, CANCEL_COUNTERPARTY_REQUEST, REMOVE_COUNTERPARTY } from '@/graphql/mutations'
|
||||
|
||||
interface Organization {
|
||||
id: string
|
||||
inn: string
|
||||
name?: string
|
||||
fullName?: string
|
||||
type: 'FULFILLMENT' | 'SELLER' | 'LOGIST' | 'WHOLESALE'
|
||||
address?: string
|
||||
phones?: Array<{ value: string }>
|
||||
emails?: Array<{ value: string }>
|
||||
createdAt: string
|
||||
users?: Array<{ id: string, avatar?: string }>
|
||||
}
|
||||
|
||||
interface CounterpartyRequest {
|
||||
id: string
|
||||
message?: string
|
||||
status: 'PENDING' | 'ACCEPTED' | 'REJECTED' | 'CANCELLED'
|
||||
createdAt: string
|
||||
sender: Organization
|
||||
receiver: Organization
|
||||
}
|
||||
|
||||
export function MarketCounterparties() {
|
||||
const { data: counterpartiesData, loading: counterpartiesLoading, refetch: refetchCounterparties } = useQuery(GET_MY_COUNTERPARTIES)
|
||||
const { data: incomingData, loading: incomingLoading, refetch: refetchIncoming } = useQuery(GET_INCOMING_REQUESTS)
|
||||
const { data: outgoingData, loading: outgoingLoading, refetch: refetchOutgoing } = useQuery(GET_OUTGOING_REQUESTS)
|
||||
|
||||
const [respondToRequest] = useMutation(RESPOND_TO_COUNTERPARTY_REQUEST, {
|
||||
onCompleted: () => {
|
||||
refetchIncoming()
|
||||
refetchCounterparties()
|
||||
}
|
||||
})
|
||||
|
||||
const [cancelRequest] = useMutation(CANCEL_COUNTERPARTY_REQUEST, {
|
||||
onCompleted: () => {
|
||||
refetchOutgoing()
|
||||
}
|
||||
})
|
||||
|
||||
const [removeCounterparty] = useMutation(REMOVE_COUNTERPARTY, {
|
||||
onCompleted: () => {
|
||||
refetchCounterparties()
|
||||
}
|
||||
})
|
||||
|
||||
const handleAcceptRequest = async (requestId: string) => {
|
||||
try {
|
||||
await respondToRequest({
|
||||
variables: { requestId, response: 'ACCEPTED' }
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка при принятии заявки:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRejectRequest = async (requestId: string) => {
|
||||
try {
|
||||
await respondToRequest({
|
||||
variables: { requestId, response: 'REJECTED' }
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка при отклонении заявки:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancelRequest = async (requestId: string) => {
|
||||
try {
|
||||
await cancelRequest({
|
||||
variables: { requestId }
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка при отмене заявки:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveCounterparty = async (organizationId: string) => {
|
||||
try {
|
||||
await removeCounterparty({
|
||||
variables: { organizationId }
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Ошибка при удалении контрагента:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
if (!dateString) return ''
|
||||
try {
|
||||
let date: Date
|
||||
|
||||
// Проверяем, является ли строка числом (Unix timestamp)
|
||||
if (/^\d+$/.test(dateString)) {
|
||||
// Если это Unix timestamp в миллисекундах
|
||||
const timestamp = parseInt(dateString, 10)
|
||||
date = new Date(timestamp)
|
||||
} else {
|
||||
// Обычная строка даты
|
||||
date = new Date(dateString)
|
||||
}
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return 'Неверная дата'
|
||||
}
|
||||
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
} catch (error) {
|
||||
return 'Ошибка даты'
|
||||
}
|
||||
}
|
||||
|
||||
const counterparties = counterpartiesData?.myCounterparties || []
|
||||
const incomingRequests = incomingData?.incomingRequests || []
|
||||
const outgoingRequests = outgoingData?.outgoingRequests || []
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex items-center space-x-3 mb-6">
|
||||
<Users className="h-6 w-6 text-blue-400" />
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white">Мои контрагенты</h3>
|
||||
<p className="text-white/60 text-sm">Управление контрагентами и заявками</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<Tabs defaultValue="counterparties" className="h-full flex flex-col">
|
||||
<TabsList className="grid w-full grid-cols-3 bg-white/5 border-white/10">
|
||||
<TabsTrigger value="counterparties" className="data-[state=active]:bg-blue-500/20 data-[state=active]:text-blue-300">
|
||||
<Users className="h-4 w-4 mr-2" />
|
||||
Контрагенты ({counterparties.length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="incoming" className="data-[state=active]:bg-green-500/20 data-[state=active]:text-green-300">
|
||||
<ArrowDownCircle className="h-4 w-4 mr-2" />
|
||||
Входящие ({incomingRequests.length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="outgoing" className="data-[state=active]:bg-orange-500/20 data-[state=active]:text-orange-300">
|
||||
<ArrowUpCircle className="h-4 w-4 mr-2" />
|
||||
Исходящие ({outgoingRequests.length})
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="counterparties" className="flex-1 overflow-auto mt-4">
|
||||
{counterpartiesLoading ? (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="text-white/60">Загрузка...</div>
|
||||
</div>
|
||||
) : counterparties.length === 0 ? (
|
||||
<div className="glass-card p-8">
|
||||
<div className="text-center">
|
||||
<Users className="h-12 w-12 text-white/20 mx-auto mb-4" />
|
||||
<p className="text-white/60">У вас пока нет контрагентов</p>
|
||||
<p className="text-white/40 text-sm mt-2">
|
||||
Перейдите на другие вкладки, чтобы найти партнеров
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{counterparties.map((organization: Organization) => (
|
||||
<OrganizationCard
|
||||
key={organization.id}
|
||||
organization={organization}
|
||||
onRemove={handleRemoveCounterparty}
|
||||
showRemoveButton={true}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="incoming" className="flex-1 overflow-auto mt-4">
|
||||
{incomingLoading ? (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="text-white/60">Загрузка...</div>
|
||||
</div>
|
||||
) : incomingRequests.length === 0 ? (
|
||||
<div className="glass-card p-8">
|
||||
<div className="text-center">
|
||||
<ArrowDownCircle className="h-12 w-12 text-white/20 mx-auto mb-4" />
|
||||
<p className="text-white/60">Нет входящих заявок</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{incomingRequests.map((request: CounterpartyRequest) => (
|
||||
<Card key={request.id} className="glass-card p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start space-x-3 flex-1">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-semibold">
|
||||
{(request.sender.name || request.sender.fullName || 'O').charAt(0).toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="text-white font-medium">
|
||||
{request.sender.name || request.sender.fullName}
|
||||
</h4>
|
||||
<p className="text-white/60 text-sm">ИНН: {request.sender.inn}</p>
|
||||
{request.message && (
|
||||
<p className="text-white/80 text-sm mt-2 italic">"{request.message}"</p>
|
||||
)}
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
<Clock className="h-3 w-3 text-white/40" />
|
||||
<span className="text-white/40 text-xs">{formatDate(request.createdAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2 ml-4">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleAcceptRequest(request.id)}
|
||||
className="bg-green-500/20 hover:bg-green-500/30 text-green-300 border-green-500/30 cursor-pointer"
|
||||
>
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleRejectRequest(request.id)}
|
||||
className="bg-red-500/20 hover:bg-red-500/30 text-red-300 border-red-500/30 cursor-pointer"
|
||||
>
|
||||
<XCircle className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="outgoing" className="flex-1 overflow-auto mt-4">
|
||||
{outgoingLoading ? (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="text-white/60">Загрузка...</div>
|
||||
</div>
|
||||
) : outgoingRequests.length === 0 ? (
|
||||
<div className="glass-card p-8">
|
||||
<div className="text-center">
|
||||
<ArrowUpCircle className="h-12 w-12 text-white/20 mx-auto mb-4" />
|
||||
<p className="text-white/60">Нет исходящих заявок</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{outgoingRequests.map((request: CounterpartyRequest) => (
|
||||
<Card key={request.id} className="glass-card p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start space-x-3 flex-1">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white font-semibold">
|
||||
{(request.receiver.name || request.receiver.fullName || 'O').charAt(0).toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="text-white font-medium">
|
||||
{request.receiver.name || request.receiver.fullName}
|
||||
</h4>
|
||||
<p className="text-white/60 text-sm">ИНН: {request.receiver.inn}</p>
|
||||
{request.message && (
|
||||
<p className="text-white/80 text-sm mt-2 italic">"{request.message}"</p>
|
||||
)}
|
||||
<div className="flex items-center space-x-4 mt-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Clock className="h-3 w-3 text-white/40" />
|
||||
<span className="text-white/40 text-xs">{formatDate(request.createdAt)}</span>
|
||||
</div>
|
||||
<Badge className={
|
||||
request.status === 'PENDING' ? 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30' :
|
||||
request.status === 'REJECTED' ? 'bg-red-500/20 text-red-300 border-red-500/30' :
|
||||
'bg-gray-500/20 text-gray-300 border-gray-500/30'
|
||||
}>
|
||||
{request.status === 'PENDING' ? 'Ожидает' : request.status === 'REJECTED' ? 'Отклонено' : request.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{request.status === 'PENDING' && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleCancelRequest(request.id)}
|
||||
className="bg-red-500/20 hover:bg-red-500/30 text-red-300 border-red-500/30 cursor-pointer ml-4"
|
||||
>
|
||||
<XCircle className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user