工单类型

This commit is contained in:
menxipeng
2025-11-09 21:29:16 +08:00
parent e5f6869e08
commit abd5a99502
3 changed files with 313 additions and 60 deletions

View File

@@ -32,7 +32,6 @@ interface UserData {
userId: string
userName: string
nickName: string
email: string
phonenumber: string
status: string
loginDate: string
@@ -189,7 +188,6 @@ export default function CompanyPermissionsPage() {
const matchesSearch =
user.nickName.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.userName.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
(user.dept?.deptName || "").toLowerCase().includes(searchTerm.toLowerCase())
const matchesStatus = statusFilter === "all" ||
@@ -264,10 +262,7 @@ export default function CompanyPermissionsPage() {
</TableCell>
<TableCell>{user.dept?.deptName || '-'}</TableCell>
<TableCell>
<div>
<div className="text-sm">{user.email || '-'}</div>
<div className="text-sm text-gray-500">{user.phonenumber || '-'}</div>
</div>
<div className="text-sm">{user.phonenumber || '-'}</div>
</TableCell>
<TableCell>
<div className="text-sm">
@@ -354,7 +349,7 @@ export default function CompanyPermissionsPage() {
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="搜索用户名、姓名、邮箱或部门..."
placeholder="搜索用户名、姓名或部门..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
@@ -438,7 +433,6 @@ function CreateUserForm({ roles, onClose }: { roles: Role[]; onClose: () => void
const [formData, setFormData] = useState({
username: "",
name: "",
email: "",
phone: "",
role: "",
department: "",
@@ -500,7 +494,6 @@ function CreateUserForm({ roles, onClose }: { roles: Role[]; onClose: () => void
nickName: formData.name,
password: formData.password,
phonenumber: formData.phone,
email: formData.email,
sex: "0",
status: "0",
remark: formData.department || "",
@@ -564,18 +557,6 @@ function CreateUserForm({ roles, onClose }: { roles: Role[]; onClose: () => void
/>
</div>
<div className="space-y-2">
<Label htmlFor="email"></Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
placeholder="请输入邮箱地址"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="phone"></Label>
<Input

View File

@@ -15,7 +15,7 @@ import {
DialogTitle,
DialogTrigger,
} from "../ui/dialog"
import { Plus, Filter, Download, MapPin, Calendar, Shield, ChevronLeft, ChevronRight } from "lucide-react"
import { Plus, Filter, Download, MapPin, Calendar, Shield, ChevronLeft, ChevronRight, Minus, Plus as PlusIcon } from "lucide-react"
import { apiGet, apiPost, apiPut } from "../../lib/services/api"
// 商户数据类型(根据新接口返回格式定义)
@@ -60,6 +60,7 @@ interface Equipment {
mallName: string
installationLocation: string
installationDate: string
nextInspectionDate: string
status: string
remarks: string
createdBy: string
@@ -201,6 +202,7 @@ export default function EquipmentPage() {
merchantId: "",
location: "",
installDate: "",
nextInspectionDate: "",
status: "normal",
notes: "",
})
@@ -213,10 +215,71 @@ export default function EquipmentPage() {
merchantId: "",
location: "",
installDate: "",
nextInspectionDate: "",
status: "normal",
notes: "",
})
// 调整下一次检测日期(添加设备)
const adjustNextInspectionDate = (years: number) => {
if (!newEquipment.nextInspectionDate) {
// 如果没有下次检测日期,使用安装日期或当前日期作为基准
const baseDate = newEquipment.installDate ? new Date(newEquipment.installDate) : new Date()
baseDate.setFullYear(baseDate.getFullYear() + years)
const newDateStr = baseDate.toISOString().split('T')[0]
// 检查是否小于安装日期
if (newEquipment.installDate && newDateStr < newEquipment.installDate) {
alert('下一次检测时间不能小于安装日期')
return
}
setNewEquipment({ ...newEquipment, nextInspectionDate: newDateStr })
} else {
const currentDate = new Date(newEquipment.nextInspectionDate)
currentDate.setFullYear(currentDate.getFullYear() + years)
const newDateStr = currentDate.toISOString().split('T')[0]
// 检查是否小于安装日期
if (newEquipment.installDate && newDateStr < newEquipment.installDate) {
alert('下一次检测时间不能小于安装日期')
return
}
setNewEquipment({ ...newEquipment, nextInspectionDate: newDateStr })
}
}
// 调整下一次检测日期(编辑设备)
const adjustEditNextInspectionDate = (years: number) => {
if (!editEquipment.nextInspectionDate) {
// 如果没有下次检测日期,使用安装日期或当前日期作为基准
const baseDate = editEquipment.installDate ? new Date(editEquipment.installDate) : new Date()
baseDate.setFullYear(baseDate.getFullYear() + years)
const newDateStr = baseDate.toISOString().split('T')[0]
// 检查是否小于安装日期
if (editEquipment.installDate && newDateStr < editEquipment.installDate) {
alert('下一次检测时间不能小于安装日期')
return
}
setEditEquipment({ ...editEquipment, nextInspectionDate: newDateStr })
} else {
const currentDate = new Date(editEquipment.nextInspectionDate)
currentDate.setFullYear(currentDate.getFullYear() + years)
const newDateStr = currentDate.toISOString().split('T')[0]
// 检查是否小于安装日期
if (editEquipment.installDate && newDateStr < editEquipment.installDate) {
alert('下一次检测时间不能小于安装日期')
return
}
setEditEquipment({ ...editEquipment, nextInspectionDate: newDateStr })
}
}
// 打开编辑对话框
const handleEditEquipment = (equipment: Equipment) => {
setEditEquipment({
@@ -227,6 +290,7 @@ export default function EquipmentPage() {
merchantId: equipment.merchantId || "",
location: equipment.installationLocation || "",
installDate: equipment.installationDate || "",
nextInspectionDate: equipment.nextInspectionDate || "",
status: equipment.status === "1" ? "normal" : equipment.status === "2" ? "expiring" : "expired",
notes: equipment.remarks || "",
})
@@ -247,6 +311,16 @@ export default function EquipmentPage() {
return
}
// 验证下一次检测时间不能小于安装日期
if (editEquipment.nextInspectionDate && editEquipment.installDate) {
const nextDate = new Date(editEquipment.nextInspectionDate)
const installDate = new Date(editEquipment.installDate)
if (nextDate < installDate) {
alert('下一次检测时间不能小于安装日期')
return
}
}
setIsSubmitting(true)
try {
@@ -258,6 +332,7 @@ export default function EquipmentPage() {
merchantId: editEquipment.merchantId,
installationLocation: editEquipment.location,
installationDate: editEquipment.installDate,
nextInspectionDate: editEquipment.nextInspectionDate,
status: editEquipment.status === 'normal' ? 1 : editEquipment.status === 'expiring' ? 2 : 3,
remarks: editEquipment.notes,
}
@@ -289,6 +364,16 @@ export default function EquipmentPage() {
return
}
// 验证下一次检测时间不能小于安装日期
if (newEquipment.nextInspectionDate && newEquipment.installDate) {
const nextDate = new Date(newEquipment.nextInspectionDate)
const installDate = new Date(newEquipment.installDate)
if (nextDate < installDate) {
alert('下一次检测时间不能小于安装日期')
return
}
}
setIsSubmitting(true)
try {
@@ -300,6 +385,7 @@ export default function EquipmentPage() {
merchantId: newEquipment.merchantId,
installationLocation: newEquipment.location,
installationDate: newEquipment.installDate,
nextInspectionDate: newEquipment.nextInspectionDate,
status: newEquipment.status === 'normal' ? 1 : newEquipment.status === 'expiring' ? 2 : 3,
remarks: newEquipment.notes,
createdBy: "admin" // 可以从用户上下文获取
@@ -322,6 +408,7 @@ export default function EquipmentPage() {
merchantId: "",
location: "",
installDate: "",
nextInspectionDate: "",
status: "normal",
notes: "",
})
@@ -421,12 +508,12 @@ export default function EquipmentPage() {
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-2 gap-x-6 gap-y-5 py-4">
<div className="space-y-2">
<Label htmlFor="equipmentName"></Label>
<Input
@@ -515,9 +602,53 @@ export default function EquipmentPage() {
id="installDate"
type="date"
value={newEquipment.installDate}
onChange={(e) => setNewEquipment({ ...newEquipment, installDate: e.target.value })}
onChange={(e) => {
const installDate = e.target.value
setNewEquipment({ ...newEquipment, installDate })
// 自动填充下一次检测时间(安装日期+1年
if (installDate && !newEquipment.nextInspectionDate) {
const nextDate = new Date(installDate)
nextDate.setFullYear(nextDate.getFullYear() + 1)
const nextDateStr = nextDate.toISOString().split('T')[0]
setNewEquipment(prev => ({ ...prev, nextInspectionDate: nextDateStr }))
}
}}
/>
</div>
<div className="col-span-2 space-y-2">
<Label htmlFor="nextInspectionDate"></Label>
<div className="flex gap-2 max-w-md">
<Input
id="nextInspectionDate"
type="date"
value={newEquipment.nextInspectionDate}
min={newEquipment.installDate || undefined}
onChange={(e) => setNewEquipment({ ...newEquipment, nextInspectionDate: e.target.value })}
className="flex-1"
/>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => adjustNextInspectionDate(-1)}
title="上一年"
className="px-3"
>
<Minus className="h-4 w-4" />
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => adjustNextInspectionDate(1)}
title="下一年"
className="px-3"
>
<PlusIcon className="h-4 w-4" />
</Button>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="status"></Label>
<Select
@@ -565,12 +696,12 @@ export default function EquipmentPage() {
{/* 编辑设备对话框 */}
<Dialog open={isEditEquipmentOpen} onOpenChange={setIsEditEquipmentOpen}>
<DialogContent className="max-w-2xl">
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-2 gap-x-6 gap-y-5 py-4">
<div className="space-y-2">
<Label htmlFor="edit-equipmentName"></Label>
<Input
@@ -642,9 +773,53 @@ export default function EquipmentPage() {
id="edit-installDate"
type="date"
value={editEquipment.installDate}
onChange={(e) => setEditEquipment({ ...editEquipment, installDate: e.target.value })}
onChange={(e) => {
const installDate = e.target.value
setEditEquipment({ ...editEquipment, installDate })
// 自动填充下一次检测时间(安装日期+1年
if (installDate && !editEquipment.nextInspectionDate) {
const nextDate = new Date(installDate)
nextDate.setFullYear(nextDate.getFullYear() + 1)
const nextDateStr = nextDate.toISOString().split('T')[0]
setEditEquipment(prev => ({ ...prev, nextInspectionDate: nextDateStr }))
}
}}
/>
</div>
<div className="col-span-2 space-y-2">
<Label htmlFor="edit-nextInspectionDate"></Label>
<div className="flex gap-2 max-w-md">
<Input
id="edit-nextInspectionDate"
type="date"
value={editEquipment.nextInspectionDate}
min={editEquipment.installDate || undefined}
onChange={(e) => setEditEquipment({ ...editEquipment, nextInspectionDate: e.target.value })}
className="flex-1"
/>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => adjustEditNextInspectionDate(-1)}
title="上一年"
className="px-3"
>
<Minus className="h-4 w-4" />
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => adjustEditNextInspectionDate(1)}
title="下一年"
className="px-3"
>
<PlusIcon className="h-4 w-4" />
</Button>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="edit-status"></Label>
<Select
@@ -842,9 +1017,21 @@ export default function EquipmentPage() {
</TableCell>
<TableCell>
<div className="space-y-1">
{item.installationDate && (
<div className="flex items-center text-sm">
<Calendar className="h-3 w-3 mr-1 text-blue-600" />
<span className="text-blue-600">: {item.createdAt}</span>
<Calendar className="h-3 w-3 mr-1 text-orange-600" />
<span className="text-orange-600">: {item.installationDate}</span>
</div>
)}
{item.nextInspectionDate && (
<div className="flex items-center text-sm">
<Calendar className="h-3 w-3 mr-1 text-green-600" />
<span className="text-green-600">: {item.nextInspectionDate}</span>
</div>
)}
<div className="flex items-center text-xs text-gray-500">
<Calendar className="h-3 w-3 mr-1" />
: {item.createdAt}
</div>
<div className="text-xs text-gray-500">: {item.updatedAt}</div>
</div>

View File

@@ -574,7 +574,7 @@ function CreateWorkOrderForm({ onClose }: { onClose: () => void }) {
merchantId: "",
equipmentId: "",
type: "",
assignee: "",
assignee: [] as string[], // 改为数组支持多选
description: "",
})
@@ -586,6 +586,7 @@ function CreateWorkOrderForm({ onClose }: { onClose: () => void }) {
const [loadingEquipment, setLoadingEquipment] = useState(false)
const [loadingWorkers, setLoadingWorkers] = useState(false)
const [submitting, setSubmitting] = useState(false)
const [isWorkerSelectOpen, setIsWorkerSelectOpen] = useState(false)
// 获取商户列表
const fetchMerchants = async () => {
@@ -637,6 +638,23 @@ function CreateWorkOrderForm({ onClose }: { onClose: () => void }) {
}
}
// 点击外部关闭下拉框
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (isWorkerSelectOpen) {
const target = event.target as HTMLElement
if (!target.closest('.worker-select-container')) {
setIsWorkerSelectOpen(false)
}
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [isWorkerSelectOpen])
const handleMerchantChange = (merchantId: string) => {
console.log('选择商户ID:', merchantId)
setSelectedMerchant(merchantId)
@@ -686,9 +704,34 @@ function CreateWorkOrderForm({ onClose }: { onClose: () => void }) {
return ["故障检测", "故障维修", "设备安装", "预防性维护", "设备改造", "设备拆除", "更换药剂"]
}
// 切换工人选择
const toggleWorkerSelection = (workerId: string) => {
setFormData(prev => ({
...prev,
assignee: prev.assignee.includes(workerId)
? prev.assignee.filter(id => id !== workerId)
: [...prev.assignee, workerId]
}))
}
// 获取选中工人的显示文本
const getSelectedWorkersText = () => {
if (formData.assignee.length === 0) return "选择维修工人"
if (formData.assignee.length === 1) {
const worker = workers.find(w => w.id === formData.assignee[0])
return worker ? worker.name : "1位工人"
}
return `已选择 ${formData.assignee.length} 位工人`
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (formData.assignee.length === 0) {
alert('请至少选择一位工人')
return
}
setSubmitting(true)
try {
@@ -696,22 +739,31 @@ function CreateWorkOrderForm({ onClose }: { onClose: () => void }) {
const selectedMerchantObj = merchants.find(m => m.id === selectedMerchant)
// 找到选中的设备信息
const selectedEquipmentObj = availableEquipment.find(e => e.equipmentId === formData.equipmentId)
// 找到选中的工人信息
const selectedWorkerObj = workers.find(w => w.id === formData.assignee)
// 找到所有选中的工人信息
const selectedWorkers = workers.filter(w => formData.assignee.includes(w.id))
if (!selectedMerchantObj || !selectedEquipmentObj || !selectedWorkerObj) {
if (!selectedMerchantObj || !selectedEquipmentObj || selectedWorkers.length === 0) {
alert('无法获取完整的商户、设备或工人信息')
return
}
// 拼接工人ID使用逗号分隔
const workersIds = selectedWorkers.map(w => w.workersId || "").join(",")
// 拼接工人名称(使用逗号分隔)
const workerNames = selectedWorkers.map(w => w.name).join(",")
// 拼接工人电话(使用逗号分隔)
const workerPhones = selectedWorkers.map(w => w.phone).join(",")
// 使用第一个工人的经销商ID
const distributorUserId = selectedWorkers[0].distributorUserId || ""
// 生成工单编号
const workOrderNumber = `WO${new Date().getFullYear()}${(new Date().getMonth() + 1).toString().padStart(2, '0')}${new Date().getDate().toString().padStart(2, '0')}${Date.now().toString().slice(-4)}`
// 构建API请求数据
const requestData = {
merchantUserId: selectedMerchantObj.merchantsId || "",
distributorUserId: selectedWorkerObj.distributorUserId || "",
workersId: selectedWorkerObj.workersId || "",
distributorUserId: distributorUserId,
workersId: workersIds, // 拼接的工人ID
workOrderNumber: workOrderNumber,
workOrderType: formData.type,
workOrderSubtype: "", // 暂时为空
@@ -724,13 +776,15 @@ function CreateWorkOrderForm({ onClose }: { onClose: () => void }) {
merchantDistrict: selectedMerchantObj.district || "",
responsiblePerson: selectedMerchantObj.contactPerson,
responsibleVirtualPhone: selectedMerchantObj.contactPhone,
workerVirtualPhone: selectedWorkerObj.phone,
workerName: selectedWorkerObj.name,
workerVirtualPhone: workerPhones, // 拼接的工人电话
workerName: workerNames, // 拼接的工人名称
createdDate: new Date().toISOString().split('T')[0],
completedDate: ""
}
console.log('创建工单请求数据:', requestData)
console.log('选中的工人数量:', selectedWorkers.length)
console.log('工人ID拼接结果:', workersIds)
const response = await apiPost('/back/orders', requestData)
@@ -834,27 +888,58 @@ function CreateWorkOrderForm({ onClose }: { onClose: () => void }) {
</div>
<div className="space-y-2">
<Label htmlFor="assignee"></Label>
<Select
value={formData.assignee}
onValueChange={(value) => setFormData({ ...formData, assignee: value })}
onOpenChange={(open) => {
if (open && workers.length === 0 && !loadingWorkers) {
<Label htmlFor="assignee"></Label>
<div className="relative worker-select-container">
<Button
type="button"
variant="outline"
className="w-full justify-between"
onClick={() => {
setIsWorkerSelectOpen(!isWorkerSelectOpen)
if (!isWorkerSelectOpen && workers.length === 0 && !loadingWorkers) {
fetchWorkers()
}
}}
>
<SelectTrigger>
<SelectValue placeholder={loadingWorkers ? "加载中..." : "选择维修工人"} />
</SelectTrigger>
<SelectContent className="max-h-[300px] overflow-y-auto">
<span className="truncate">{loadingWorkers ? "加载中..." : getSelectedWorkersText()}</span>
<ChevronRight className={`h-4 w-4 ml-2 transition-transform ${isWorkerSelectOpen ? 'rotate-90' : ''}`} />
</Button>
{isWorkerSelectOpen && (
<div className="absolute z-50 w-full mt-1 bg-white border rounded-md shadow-lg max-h-[300px] overflow-y-auto">
{workers.length === 0 ? (
<div className="p-4 text-center text-gray-500"></div>
) : (
<div className="p-2">
{workers.map((worker) => (
<SelectItem key={worker.id} value={worker.id}>
<div
key={worker.id}
className="flex items-center space-x-2 p-2 hover:bg-gray-100 rounded cursor-pointer"
onClick={() => toggleWorkerSelection(worker.id)}
>
<div className={`w-4 h-4 border rounded flex items-center justify-center ${
formData.assignee.includes(worker.id) ? 'bg-blue-600 border-blue-600' : 'border-gray-300'
}`}>
{formData.assignee.includes(worker.id) && (
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
)}
</div>
<span className="text-sm flex-1">
{worker.name} - {worker.province} ({worker.phone})
</SelectItem>
</span>
</div>
))}
</SelectContent>
</Select>
</div>
)}
</div>
)}
</div>
{formData.assignee.length > 0 && (
<div className="text-xs text-gray-500 mt-1">
: {workers.filter(w => formData.assignee.includes(w.id)).map(w => w.name).join(", ")}
</div>
)}
</div>
</div>