initial commit
This commit is contained in:
665
components/admin/ServiceManagement.tsx
Normal file
665
components/admin/ServiceManagement.tsx
Normal file
@@ -0,0 +1,665 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Edit, X, ChevronLeft, ChevronRight, Server, Users, Activity } from 'lucide-react'
|
||||
import { toast } from '@/hooks/use-toast'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
|
||||
interface BillingService {
|
||||
_id: string
|
||||
user_email: string
|
||||
silicon_id: string
|
||||
service_type: string
|
||||
service_name: string
|
||||
amount: number
|
||||
service_status: 'active' | 'inactive' | 'suspended' | 'cancelled'
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface DeveloperRequest {
|
||||
_id: string
|
||||
userId: {
|
||||
name: string
|
||||
email: string
|
||||
siliconId: string
|
||||
}
|
||||
planType: string
|
||||
projectDescription: string
|
||||
status: 'pending' | 'approved' | 'in_progress' | 'completed' | 'cancelled'
|
||||
assignedDeveloper?:
|
||||
| {
|
||||
name: string
|
||||
email: string
|
||||
skills: string[]
|
||||
}
|
||||
| string
|
||||
estimatedCompletionDate?: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
interface ServiceStats {
|
||||
_id: string
|
||||
count: number
|
||||
revenue: number
|
||||
activeServices: number
|
||||
}
|
||||
|
||||
export default function ServiceManagement() {
|
||||
const [billingServices, setBillingServices] = useState<BillingService[]>([])
|
||||
const [developerRequests, setDeveloperRequests] = useState<DeveloperRequest[]>([])
|
||||
const [serviceStats, setServiceStats] = useState<ServiceStats[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [serviceTypeFilter, setServiceTypeFilter] = useState('all')
|
||||
const [statusFilter, setStatusFilter] = useState('all')
|
||||
const [dateFrom, setDateFrom] = useState('')
|
||||
const [dateTo, setDateTo] = useState('')
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
const [editingService, setEditingService] = useState<any>(null)
|
||||
const [editForm, setEditForm] = useState({
|
||||
status: '',
|
||||
assignedDeveloper: '',
|
||||
estimatedCompletionDate: '',
|
||||
notes: '',
|
||||
})
|
||||
|
||||
const fetchServices = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const token =
|
||||
localStorage.getItem('token') ||
|
||||
document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('token='))
|
||||
?.split('=')[1]
|
||||
|
||||
const params = new URLSearchParams({
|
||||
page: currentPage.toString(),
|
||||
limit: '20',
|
||||
serviceType: serviceTypeFilter,
|
||||
status: statusFilter,
|
||||
...(dateFrom && { dateFrom }),
|
||||
...(dateTo && { dateTo }),
|
||||
})
|
||||
|
||||
const response = await fetch(`/api/admin/services?${params}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch services data')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
setBillingServices(data.billingServices)
|
||||
setDeveloperRequests(data.developerRequests)
|
||||
setServiceStats(data.serviceStats)
|
||||
setTotalPages(data.pagination.totalPages)
|
||||
} catch (error) {
|
||||
console.error('Services fetch error:', error)
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to load services data',
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [serviceTypeFilter, statusFilter, dateFrom, dateTo, currentPage])
|
||||
|
||||
useEffect(() => {
|
||||
fetchServices()
|
||||
}, [fetchServices])
|
||||
|
||||
const handleEditService = (service: any, type: 'billing' | 'developer') => {
|
||||
setEditingService({ ...service, type })
|
||||
if (type === 'billing') {
|
||||
setEditForm({
|
||||
status: service.service_status,
|
||||
assignedDeveloper: '',
|
||||
estimatedCompletionDate: '',
|
||||
notes: '',
|
||||
})
|
||||
} else {
|
||||
setEditForm({
|
||||
status: service.status,
|
||||
assignedDeveloper:
|
||||
typeof service.assignedDeveloper === 'object' && service.assignedDeveloper?.name
|
||||
? service.assignedDeveloper.name
|
||||
: typeof service.assignedDeveloper === 'string'
|
||||
? service.assignedDeveloper
|
||||
: '',
|
||||
estimatedCompletionDate: service.estimatedCompletionDate || '',
|
||||
notes: '',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateService = async () => {
|
||||
if (!editingService) return
|
||||
|
||||
try {
|
||||
const token =
|
||||
localStorage.getItem('token') ||
|
||||
document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('token='))
|
||||
?.split('=')[1]
|
||||
|
||||
const action = editingService.type === 'billing' ? 'updateBilling' : 'updateDeveloperRequest'
|
||||
|
||||
const response = await fetch('/api/admin/services', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action,
|
||||
serviceId: editingService._id,
|
||||
serviceType: editingService.type,
|
||||
data:
|
||||
editingService.type === 'billing'
|
||||
? { service_status: editForm.status }
|
||||
: {
|
||||
status: editForm.status,
|
||||
assignedDeveloper: editForm.assignedDeveloper,
|
||||
estimatedCompletionDate: editForm.estimatedCompletionDate,
|
||||
notes: editForm.notes,
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update service')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (editingService.type === 'billing') {
|
||||
setBillingServices(
|
||||
billingServices.map((s) => (s._id === editingService._id ? data.service : s))
|
||||
)
|
||||
} else {
|
||||
setDeveloperRequests(
|
||||
developerRequests.map((s) => (s._id === editingService._id ? data.service : s))
|
||||
)
|
||||
}
|
||||
|
||||
setEditingService(null)
|
||||
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Service updated successfully',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Service update error:', error)
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to update service',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancelService = async (serviceId: string, type: 'billing' | 'developer') => {
|
||||
if (!confirm('Are you sure you want to cancel this service?')) return
|
||||
|
||||
try {
|
||||
const token =
|
||||
localStorage.getItem('token') ||
|
||||
document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('token='))
|
||||
?.split('=')[1]
|
||||
|
||||
const response = await fetch('/api/admin/services', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'cancelService',
|
||||
serviceId,
|
||||
serviceType: type,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to cancel service')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (type === 'billing') {
|
||||
setBillingServices(billingServices.map((s) => (s._id === serviceId ? data.service : s)))
|
||||
} else {
|
||||
setDeveloperRequests(developerRequests.map((s) => (s._id === serviceId ? data.service : s)))
|
||||
}
|
||||
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Service cancelled successfully',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Service cancel error:', error)
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to cancel service',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('en-IN', {
|
||||
style: 'currency',
|
||||
currency: 'INR',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
const getStatusBadgeColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
case 'completed':
|
||||
return 'bg-green-100 text-green-800'
|
||||
case 'pending':
|
||||
case 'approved':
|
||||
return 'bg-yellow-100 text-yellow-800'
|
||||
case 'in_progress':
|
||||
return 'bg-blue-100 text-blue-800'
|
||||
case 'cancelled':
|
||||
case 'inactive':
|
||||
return 'bg-red-100 text-red-800'
|
||||
case 'suspended':
|
||||
return 'bg-orange-100 text-orange-800'
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Service Statistics */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{serviceStats.map((stat) => (
|
||||
<Card key={stat._id}>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 capitalize">{stat._id.replace('_', ' ')}</p>
|
||||
<p className="text-2xl font-bold">{stat.count}</p>
|
||||
<p className="text-sm text-green-600">
|
||||
{stat.activeServices} active • {formatCurrency(stat.revenue)}
|
||||
</p>
|
||||
</div>
|
||||
<Server className="h-8 w-8 text-blue-600" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Service Management</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<Select value={serviceTypeFilter} onValueChange={setServiceTypeFilter}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Service Type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Services</SelectItem>
|
||||
<SelectItem value="vps_deployment">VPS</SelectItem>
|
||||
<SelectItem value="kubernetes_deployment">Kubernetes</SelectItem>
|
||||
<SelectItem value="developer_hire">Developer Hire</SelectItem>
|
||||
<SelectItem value="vpn_service">VPN</SelectItem>
|
||||
<SelectItem value="hosting_config">Hosting</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Status</SelectItem>
|
||||
<SelectItem value="active">Active</SelectItem>
|
||||
<SelectItem value="pending">Pending</SelectItem>
|
||||
<SelectItem value="in_progress">In Progress</SelectItem>
|
||||
<SelectItem value="completed">Completed</SelectItem>
|
||||
<SelectItem value="cancelled">Cancelled</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
type="date"
|
||||
placeholder="From Date"
|
||||
value={dateFrom}
|
||||
onChange={(e) => setDateFrom(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
type="date"
|
||||
placeholder="To Date"
|
||||
value={dateTo}
|
||||
onChange={(e) => setDateTo(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Billing Services */}
|
||||
<div className="mb-8">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center">
|
||||
<Activity className="h-5 w-5 mr-2" />
|
||||
Billing Services
|
||||
</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b">
|
||||
<th className="text-left py-3 px-4">Customer</th>
|
||||
<th className="text-left py-3 px-4">Service</th>
|
||||
<th className="text-left py-3 px-4">Amount</th>
|
||||
<th className="text-left py-3 px-4">Status</th>
|
||||
<th className="text-left py-3 px-4">Date</th>
|
||||
<th className="text-left py-3 px-4">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{billingServices.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="text-center py-8 text-gray-500">
|
||||
No billing services found
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
billingServices.map((service) => (
|
||||
<tr key={service._id} className="border-b hover:bg-gray-50">
|
||||
<td className="py-3 px-4">
|
||||
<div>
|
||||
<p className="font-medium">{service.user_email}</p>
|
||||
<p className="text-sm text-gray-500">ID: {service.silicon_id}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<div>
|
||||
<p className="font-medium">{service.service_name}</p>
|
||||
<p className="text-sm text-gray-500 capitalize">
|
||||
{service.service_type.replace('_', ' ')}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className="font-medium">{formatCurrency(service.amount)}</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<Badge className={getStatusBadgeColor(service.service_status)}>
|
||||
{service.service_status}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className="text-sm text-gray-600">
|
||||
{service.created_at
|
||||
? formatDistanceToNow(new Date(service.created_at), {
|
||||
addSuffix: true,
|
||||
})
|
||||
: 'N/A'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleEditService(service, 'billing')}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleCancelService(service._id, 'billing')}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Developer Requests */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center">
|
||||
<Users className="h-5 w-5 mr-2" />
|
||||
Developer Requests
|
||||
</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b">
|
||||
<th className="text-left py-3 px-4">Customer</th>
|
||||
<th className="text-left py-3 px-4">Plan</th>
|
||||
<th className="text-left py-3 px-4">Description</th>
|
||||
<th className="text-left py-3 px-4">Status</th>
|
||||
<th className="text-left py-3 px-4">Developer</th>
|
||||
<th className="text-left py-3 px-4">Date</th>
|
||||
<th className="text-left py-3 px-4">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{developerRequests.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="text-center py-8 text-gray-500">
|
||||
No developer requests found
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
developerRequests.map((request) => (
|
||||
<tr key={request._id} className="border-b hover:bg-gray-50">
|
||||
<td className="py-3 px-4">
|
||||
<div>
|
||||
<p className="font-medium">{request.userId.name}</p>
|
||||
<p className="text-sm text-gray-500">{request.userId.email}</p>
|
||||
<p className="text-xs text-gray-400">ID: {request.userId.siliconId}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<Badge variant="outline">{request.planType}</Badge>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<p className="text-sm max-w-xs truncate">{request.projectDescription}</p>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<Badge className={getStatusBadgeColor(request.status)}>
|
||||
{request.status}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className="text-sm">
|
||||
{typeof request.assignedDeveloper === 'object' &&
|
||||
request.assignedDeveloper?.name
|
||||
? request.assignedDeveloper.name
|
||||
: typeof request.assignedDeveloper === 'string'
|
||||
? request.assignedDeveloper
|
||||
: 'Unassigned'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className="text-sm text-gray-600">
|
||||
{request.createdAt
|
||||
? formatDistanceToNow(new Date(request.createdAt), {
|
||||
addSuffix: true,
|
||||
})
|
||||
: 'N/A'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleEditService(request, 'developer')}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleCancelService(request._id, 'developer')}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between mt-6">
|
||||
<p className="text-sm text-gray-600">
|
||||
Page {currentPage} of {totalPages}
|
||||
</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
Next
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Edit Service Dialog */}
|
||||
<Dialog open={!!editingService} onOpenChange={() => setEditingService(null)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Edit {editingService?.type === 'billing' ? 'Service' : 'Developer Request'}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="status">Status</Label>
|
||||
<Select
|
||||
value={editForm.status}
|
||||
onValueChange={(value) => setEditForm({ ...editForm, status: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{editingService?.type === 'billing' ? (
|
||||
<>
|
||||
<SelectItem value="active">Active</SelectItem>
|
||||
<SelectItem value="inactive">Inactive</SelectItem>
|
||||
<SelectItem value="suspended">Suspended</SelectItem>
|
||||
<SelectItem value="cancelled">Cancelled</SelectItem>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<SelectItem value="pending">Pending</SelectItem>
|
||||
<SelectItem value="approved">Approved</SelectItem>
|
||||
<SelectItem value="in_progress">In Progress</SelectItem>
|
||||
<SelectItem value="completed">Completed</SelectItem>
|
||||
<SelectItem value="cancelled">Cancelled</SelectItem>
|
||||
</>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{editingService?.type === 'developer' && (
|
||||
<>
|
||||
<div>
|
||||
<Label htmlFor="assignedDeveloper">Assigned Developer</Label>
|
||||
<Input
|
||||
id="assignedDeveloper"
|
||||
value={editForm.assignedDeveloper}
|
||||
onChange={(e) =>
|
||||
setEditForm({ ...editForm, assignedDeveloper: e.target.value })
|
||||
}
|
||||
placeholder="Developer name or ID"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="estimatedCompletionDate">Estimated Completion Date</Label>
|
||||
<Input
|
||||
id="estimatedCompletionDate"
|
||||
type="date"
|
||||
value={editForm.estimatedCompletionDate}
|
||||
onChange={(e) =>
|
||||
setEditForm({ ...editForm, estimatedCompletionDate: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="notes">Notes</Label>
|
||||
<Textarea
|
||||
id="notes"
|
||||
value={editForm.notes}
|
||||
onChange={(e) => setEditForm({ ...editForm, notes: e.target.value })}
|
||||
placeholder="Add notes about this update..."
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button variant="outline" onClick={() => setEditingService(null)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleUpdateService}>Update Service</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user