fix alert api
Some checks failed
Deploy MyApp on Same Server / build-and-deploy (push) Failing after 1s
Some checks failed
Deploy MyApp on Same Server / build-and-deploy (push) Failing after 1s
This commit is contained in:
@@ -6,7 +6,8 @@ import {
|
|||||||
AlertConditionDto,
|
AlertConditionDto,
|
||||||
CreateAlertConditionDto,
|
CreateAlertConditionDto,
|
||||||
UpdateAlertConditionDto,
|
UpdateAlertConditionDto,
|
||||||
AlertRuleDto
|
AlertRuleDto,
|
||||||
|
CreateAlertRuleRequest
|
||||||
} from '@/lib/api'
|
} from '@/lib/api'
|
||||||
import {
|
import {
|
||||||
Bell,
|
Bell,
|
||||||
@@ -77,7 +78,7 @@ function AlertSettingsContent() {
|
|||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
notificationType: 0,
|
notificationType: 0,
|
||||||
timeType: 2,
|
timeType: 2,
|
||||||
isActive: true,
|
isEnabled: true,
|
||||||
rules: []
|
rules: []
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -103,11 +104,12 @@ function AlertSettingsContent() {
|
|||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
notificationType: 0,
|
notificationType: 0,
|
||||||
timeType: 2,
|
timeType: 2,
|
||||||
isActive: true,
|
isEnabled: true,
|
||||||
rules: [{
|
rules: [{
|
||||||
sensorType: 0,
|
sensorType: 0,
|
||||||
comparisonType: 0,
|
comparisonType: 0,
|
||||||
threshold: 30
|
value1: 30,
|
||||||
|
order: 0
|
||||||
}]
|
}]
|
||||||
})
|
})
|
||||||
setShowModal(true)
|
setShowModal(true)
|
||||||
@@ -116,11 +118,17 @@ function AlertSettingsContent() {
|
|||||||
const openEditModal = (alert: AlertConditionDto) => {
|
const openEditModal = (alert: AlertConditionDto) => {
|
||||||
setEditingAlert(alert)
|
setEditingAlert(alert)
|
||||||
setFormData({
|
setFormData({
|
||||||
deviceId: alert.deviceId,
|
|
||||||
notificationType: alert.notificationType,
|
notificationType: alert.notificationType,
|
||||||
timeType: alert.timeType,
|
timeType: alert.timeType,
|
||||||
isActive: alert.isActive,
|
isEnabled: alert.isEnabled,
|
||||||
rules: alert.rules.map(r => ({ ...r }))
|
deviceId: alert.deviceId,
|
||||||
|
rules: alert.rules.map((r, idx) => ({
|
||||||
|
sensorType: r.sensorType,
|
||||||
|
comparisonType: r.comparisonType,
|
||||||
|
value1: r.value1,
|
||||||
|
value2: r.value2,
|
||||||
|
order: idx
|
||||||
|
}))
|
||||||
})
|
})
|
||||||
setShowModal(true)
|
setShowModal(true)
|
||||||
}
|
}
|
||||||
@@ -181,11 +189,16 @@ function AlertSettingsContent() {
|
|||||||
try {
|
try {
|
||||||
const updateDto: UpdateAlertConditionDto = {
|
const updateDto: UpdateAlertConditionDto = {
|
||||||
id: alert.id,
|
id: alert.id,
|
||||||
deviceId: alert.deviceId,
|
|
||||||
notificationType: alert.notificationType,
|
notificationType: alert.notificationType,
|
||||||
timeType: alert.timeType,
|
timeType: alert.timeType,
|
||||||
isActive: !alert.isActive,
|
isEnabled: !alert.isEnabled,
|
||||||
rules: alert.rules
|
rules: alert.rules.map((r, idx) => ({
|
||||||
|
sensorType: r.sensorType,
|
||||||
|
comparisonType: r.comparisonType,
|
||||||
|
value1: r.value1,
|
||||||
|
value2: r.value2,
|
||||||
|
order: idx
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
await api.updateAlertCondition(updateDto)
|
await api.updateAlertCondition(updateDto)
|
||||||
await loadAlerts()
|
await loadAlerts()
|
||||||
@@ -203,20 +216,24 @@ function AlertSettingsContent() {
|
|||||||
{
|
{
|
||||||
sensorType: 0,
|
sensorType: 0,
|
||||||
comparisonType: 0,
|
comparisonType: 0,
|
||||||
threshold: 30
|
value1: 30,
|
||||||
|
order: formData.rules.length
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeRule = (index: number) => {
|
const removeRule = (index: number) => {
|
||||||
|
const newRules = formData.rules
|
||||||
|
.filter((_, i) => i !== index)
|
||||||
|
.map((r, idx) => ({ ...r, order: idx }))
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
rules: formData.rules.filter((_, i) => i !== index)
|
rules: newRules
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateRule = (index: number, updates: Partial<AlertRuleDto>) => {
|
const updateRule = (index: number, updates: Partial<CreateAlertRuleRequest>) => {
|
||||||
const newRules = [...formData.rules]
|
const newRules = [...formData.rules]
|
||||||
newRules[index] = { ...newRules[index], ...updates }
|
newRules[index] = { ...newRules[index], ...updates }
|
||||||
setFormData({ ...formData, rules: newRules })
|
setFormData({ ...formData, rules: newRules })
|
||||||
@@ -255,51 +272,62 @@ function AlertSettingsContent() {
|
|||||||
const formatRuleText = (rule: AlertRuleDto) => {
|
const formatRuleText = (rule: AlertRuleDto) => {
|
||||||
const comp = COMPARISON_TYPES.find(c => c.value === rule.comparisonType)
|
const comp = COMPARISON_TYPES.find(c => c.value === rule.comparisonType)
|
||||||
if (comp?.needsMax) {
|
if (comp?.needsMax) {
|
||||||
return `${getComparisonLabel(rule.comparisonType)}: ${rule.threshold} - ${rule.thresholdMax ?? '?'} ${getSensorUnit(rule.sensorType)}`
|
return `${getComparisonLabel(rule.comparisonType)}: ${rule.value1} - ${rule.value2 ?? '?'} ${getSensorUnit(rule.sensorType)}`
|
||||||
}
|
}
|
||||||
const symbol = rule.comparisonType === 0 ? '>' : rule.comparisonType === 1 ? '<' : '?'
|
const symbol = rule.comparisonType === 0 ? '>' : rule.comparisonType === 1 ? '<' : '?'
|
||||||
return `${symbol} ${rule.threshold} ${getSensorUnit(rule.sensorType)}`
|
return `${symbol} ${rule.value1} ${getSensorUnit(rule.sensorType)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const generatePreviewText = () => {
|
const generatePreviewText = () => {
|
||||||
if (formData.rules.length === 0) {
|
if (formData.rules.length === 0) {
|
||||||
return 'هنوز هیچ شرطی تعریف نشده است.'
|
return '🤔 هنوز شرطی تعریف نکردی!'
|
||||||
}
|
}
|
||||||
|
|
||||||
// نوع اطلاعرسانی
|
// نوع اطلاعرسانی
|
||||||
const notifText = formData.notificationType === 0 ? 'تماس تلفنی' : 'پیامک'
|
const notifText = formData.notificationType === 0 ? 'بهت زنگ بزنم' : 'برات پیامک بفرستم'
|
||||||
|
|
||||||
// زمان
|
// زمان
|
||||||
const timeText = formData.timeType === 0
|
const timeText = formData.timeType === 0
|
||||||
? 'در روز'
|
? 'فقط توی روز'
|
||||||
: formData.timeType === 1
|
: formData.timeType === 1
|
||||||
? 'در شب'
|
? 'فقط توی شب'
|
||||||
: 'در هر زمان'
|
: ''
|
||||||
|
|
||||||
// قوانین
|
// قوانین
|
||||||
const rulesText = formData.rules.map(rule => {
|
const rulesText = formData.rules.map(rule => {
|
||||||
const sensorName = getSensorLabel(rule.sensorType)
|
const sensorName = getSensorLabel(rule.sensorType)
|
||||||
|
const unit = getSensorUnit(rule.sensorType)
|
||||||
const comp = COMPARISON_TYPES.find(c => c.value === rule.comparisonType)
|
const comp = COMPARISON_TYPES.find(c => c.value === rule.comparisonType)
|
||||||
|
|
||||||
if (comp?.needsMax) {
|
if (comp?.needsMax) {
|
||||||
// بین یا خارج از محدوده
|
// بین یا خارج از محدوده
|
||||||
const compText = rule.comparisonType === 2 ? 'بین' : 'خارج از'
|
if (rule.comparisonType === 2) {
|
||||||
return `${sensorName} ${compText} ${rule.threshold} تا ${rule.thresholdMax ?? '؟'} ${getSensorUnit(rule.sensorType)}`
|
return `${sensorName} بین ${rule.value1} تا ${rule.value2 ?? '؟'} ${unit} باشه`
|
||||||
|
} else {
|
||||||
|
return `${sensorName} از بازه ${rule.value1} تا ${rule.value2 ?? '؟'} ${unit} خارج شه`
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// بزرگتر یا کوچکتر
|
// بزرگتر یا کوچکتر
|
||||||
const compText = rule.comparisonType === 0 ? 'بیشتر از' : 'کمتر از'
|
const compText = rule.comparisonType === 0 ? 'از' : 'از'
|
||||||
return `${sensorName} ${compText} ${rule.threshold} ${getSensorUnit(rule.sensorType)}`
|
const direction = rule.comparisonType === 0 ? 'بالاتر' : 'پایینتر'
|
||||||
|
return `${sensorName} ${direction} ${compText} ${rule.value1} ${unit} بره`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// ساخت جمله نهایی
|
// ساخت جمله نهایی
|
||||||
|
let baseText = ''
|
||||||
if (rulesText.length === 1) {
|
if (rulesText.length === 1) {
|
||||||
return `ارسال ${notifText} برای زمانی که ${timeText} ${rulesText[0]} باشد.`
|
baseText = `وقتی ${rulesText[0]}`
|
||||||
} else {
|
} else {
|
||||||
const lastRule = rulesText[rulesText.length - 1]
|
const lastRule = rulesText[rulesText.length - 1]
|
||||||
const otherRules = rulesText.slice(0, -1).join(' و ')
|
const otherRules = rulesText.slice(0, -1).join(' و ')
|
||||||
return `ارسال ${notifText} برای زمانی که ${timeText} ${otherRules} و ${lastRule} باشند.`
|
baseText = `وقتی ${otherRules} و ${lastRule}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// اضافه کردن زمان اگر مشخص شده
|
||||||
|
const timePrefix = timeText ? `${timeText}، ` : ''
|
||||||
|
|
||||||
|
return `${timePrefix}${baseText}، ${notifText} 📱`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -358,7 +386,7 @@ function AlertSettingsContent() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-gray-200">
|
<div className="divide-y divide-gray-200">
|
||||||
{alerts.map((alert) => (
|
{alerts.map((alert) => (
|
||||||
<div key={alert.id} className={`p-6 hover:bg-gray-50 transition-colors ${!alert.isActive ? 'opacity-50' : ''}`}>
|
<div key={alert.id} className={`p-6 hover:bg-gray-50 transition-colors ${!alert.isEnabled ? 'opacity-50' : ''}`}>
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div className="flex-1 space-y-3">
|
<div className="flex-1 space-y-3">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -388,12 +416,12 @@ function AlertSettingsContent() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => toggleActive(alert)}
|
onClick={() => toggleActive(alert)}
|
||||||
className={`inline-flex items-center gap-1 px-3 py-1 rounded-full text-sm font-medium transition-colors ${
|
className={`inline-flex items-center gap-1 px-3 py-1 rounded-full text-sm font-medium transition-colors ${
|
||||||
alert.isActive
|
alert.isEnabled
|
||||||
? 'bg-green-100 text-green-800 hover:bg-green-200'
|
? 'bg-green-100 text-green-800 hover:bg-green-200'
|
||||||
: 'bg-gray-100 text-gray-800 hover:bg-gray-200'
|
: 'bg-gray-100 text-gray-800 hover:bg-gray-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{alert.isActive ? (
|
{alert.isEnabled ? (
|
||||||
<>
|
<>
|
||||||
<Check className="w-3 h-3" />
|
<Check className="w-3 h-3" />
|
||||||
فعال
|
فعال
|
||||||
@@ -468,7 +496,25 @@ function AlertSettingsContent() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal Body */}
|
{/* Modal Body */}
|
||||||
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
{/* Preview - Sticky at top */}
|
||||||
|
<div className="sticky top-[73px] z-10 bg-gradient-to-r from-blue-500 to-indigo-600 shadow-lg">
|
||||||
|
<div className="px-6 py-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="flex-shrink-0 w-10 h-10 bg-white/20 rounded-lg flex items-center justify-center backdrop-blur-sm">
|
||||||
|
<Bell className="w-5 h-5 text-white" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-xs font-semibold text-white/90 mb-1.5">📢 پیشنمایش زنده</div>
|
||||||
|
<div className="text-sm md:text-base text-white leading-relaxed font-medium">
|
||||||
|
{generatePreviewText()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
{/* Notification Type */}
|
{/* Notification Type */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-3">
|
<label className="block text-sm font-medium text-gray-700 mb-3">
|
||||||
@@ -556,20 +602,26 @@ function AlertSettingsContent() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-3">
|
||||||
{formData.rules.map((rule, index) => {
|
{formData.rules.map((rule, index) => {
|
||||||
const needsMax = COMPARISON_TYPES.find(c => c.value === rule.comparisonType)?.needsMax
|
const needsMax = COMPARISON_TYPES.find(c => c.value === rule.comparisonType)?.needsMax
|
||||||
|
const selectedSensor = SENSOR_TYPES.find(s => s.value === rule.sensorType)
|
||||||
|
const SensorIcon = selectedSensor?.icon
|
||||||
|
const selectedComp = COMPARISON_TYPES.find(c => c.value === rule.comparisonType)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={index} className="bg-gray-50 rounded-lg p-4 space-y-4 relative">
|
<div key={index} className="bg-gradient-to-br from-orange-50 to-red-50 border-2 border-orange-200 rounded-xl p-4 space-y-3 relative shadow-sm">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span className="text-sm font-medium text-gray-700">
|
<div className="flex items-center gap-2">
|
||||||
{formData.rules.length === 1 ? 'شرط' : `شرط ${index + 1}`}
|
<span className="text-xs font-bold text-orange-700 bg-white px-2.5 py-1 rounded-full shadow-sm">
|
||||||
</span>
|
{formData.rules.length === 1 ? '⚡ شرط' : `⚡ شرط ${index + 1}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
{formData.rules.length > 1 && (
|
{formData.rules.length > 1 && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => removeRule(index)}
|
onClick={() => removeRule(index)}
|
||||||
className="p-1 text-red-600 hover:bg-red-50 rounded transition-colors"
|
className="p-2 text-red-600 hover:bg-white/50 rounded-lg transition-colors"
|
||||||
title="حذف این شرط"
|
title="حذف این شرط"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
@@ -577,12 +629,12 @@ function AlertSettingsContent() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sensor Type */}
|
{/* Sensor Type - Dropdown Style */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-600 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
کدام سنسور را میخواهید بررسی کنید؟
|
🎯 کدام سنسور را بررسی کنیم؟
|
||||||
</label>
|
</label>
|
||||||
<div className="grid grid-cols-3 md:grid-cols-5 gap-2">
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-2">
|
||||||
{SENSOR_TYPES.map(sensor => {
|
{SENSOR_TYPES.map(sensor => {
|
||||||
const Icon = sensor.icon
|
const Icon = sensor.icon
|
||||||
const isSelected = rule.sensorType === sensor.value
|
const isSelected = rule.sensorType === sensor.value
|
||||||
@@ -591,24 +643,26 @@ function AlertSettingsContent() {
|
|||||||
key={sensor.value}
|
key={sensor.value}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateRule(index, { sensorType: sensor.value })}
|
onClick={() => updateRule(index, { sensorType: sensor.value })}
|
||||||
className={`flex flex-col items-center gap-1 p-2 rounded border transition-all ${
|
className={`flex flex-col items-center gap-2 p-3 rounded-lg border-2 transition-all ${
|
||||||
isSelected
|
isSelected
|
||||||
? 'border-orange-500 bg-orange-50'
|
? 'border-orange-500 bg-white shadow-md scale-105'
|
||||||
: 'border-gray-200 hover:border-gray-300'
|
: 'border-white/50 bg-white/50 hover:border-orange-300 hover:bg-white'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Icon className={`w-4 h-4 ${isSelected ? 'text-orange-500' : 'text-gray-400'}`} />
|
<Icon className={`w-6 h-6 ${isSelected ? 'text-orange-500' : 'text-gray-400'}`} />
|
||||||
<span className="text-xs">{sensor.label}</span>
|
<span className={`text-xs font-medium ${isSelected ? 'text-orange-700' : 'text-gray-600'}`}>
|
||||||
|
{sensor.label}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Comparison Type */}
|
{/* Comparison Type - Card Style */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-600 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
چه زمانی باید هشدار داد؟
|
📊 چه موقع هشدار بدهیم؟
|
||||||
</label>
|
</label>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||||
{COMPARISON_TYPES.map(comp => {
|
{COMPARISON_TYPES.map(comp => {
|
||||||
@@ -619,50 +673,72 @@ function AlertSettingsContent() {
|
|||||||
key={comp.value}
|
key={comp.value}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateRule(index, { comparisonType: comp.value })}
|
onClick={() => updateRule(index, { comparisonType: comp.value })}
|
||||||
className={`flex flex-col items-center gap-1 p-3 rounded border transition-all ${
|
className={`flex flex-col items-center gap-2 p-3 rounded-lg border-2 transition-all ${
|
||||||
isSelected
|
isSelected
|
||||||
? 'border-orange-500 bg-orange-50'
|
? 'border-orange-500 bg-white shadow-md scale-105'
|
||||||
: 'border-gray-200 hover:border-gray-300'
|
: 'border-white/50 bg-white/50 hover:border-orange-300 hover:bg-white'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Icon className={`w-5 h-5 ${isSelected ? 'text-orange-500' : 'text-gray-400'}`} />
|
{'length' in Icon && Icon.length === 0 ? (
|
||||||
<div className="text-xs text-center leading-tight">{comp.label}</div>
|
<span className={`text-xl ${isSelected ? 'text-orange-500' : 'text-gray-400'}`}>
|
||||||
|
{(Icon as () => React.JSX.Element)()}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
(() => {
|
||||||
|
const IconComponent = Icon as LucideIcon
|
||||||
|
return <IconComponent className={`w-6 h-6 ${isSelected ? 'text-orange-500' : 'text-gray-400'}`} />
|
||||||
|
})()
|
||||||
|
)}
|
||||||
|
<span className={`text-xs font-medium text-center ${isSelected ? 'text-orange-700' : 'text-gray-600'}`}>
|
||||||
|
{comp.label}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Threshold */}
|
{/* Threshold Values */}
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div>
|
||||||
<div>
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
<label className="block text-xs font-medium text-gray-600 mb-2">
|
🔢 مقدار آستانه
|
||||||
{needsMax ? 'از چه مقداری' : 'چه مقداری'} ({getSensorUnit(rule.sensorType)})
|
</label>
|
||||||
</label>
|
<div className="flex gap-2">
|
||||||
<input
|
<div className="flex-1">
|
||||||
type="number"
|
<div className="relative">
|
||||||
step="0.01"
|
<input
|
||||||
value={rule.threshold}
|
type="number"
|
||||||
onChange={(e) => updateRule(index, { threshold: Number(e.target.value) })}
|
step="0.01"
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-transparent text-sm"
|
value={rule.value1}
|
||||||
required
|
onChange={(e) => updateRule(index, { value1: Number(e.target.value) })}
|
||||||
/>
|
className="w-full px-4 py-3 pr-16 border-2 border-white bg-white rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500 text-base font-medium"
|
||||||
</div>
|
placeholder={needsMax ? 'از مقدار...' : 'مقدار...'}
|
||||||
{needsMax && (
|
required
|
||||||
<div>
|
/>
|
||||||
<label className="block text-xs font-medium text-gray-600 mb-2">
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-sm text-gray-500 font-medium">
|
||||||
تا چه مقداری ({getSensorUnit(rule.sensorType)})
|
{getSensorUnit(rule.sensorType)}
|
||||||
</label>
|
</span>
|
||||||
<input
|
</div>
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
value={rule.thresholdMax ?? ''}
|
|
||||||
onChange={(e) => updateRule(index, { thresholdMax: Number(e.target.value) })}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-transparent text-sm"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
{needsMax && (
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={rule.value2 ?? ''}
|
||||||
|
onChange={(e) => updateRule(index, { value2: Number(e.target.value) })}
|
||||||
|
className="w-full px-4 py-3 pr-16 border-2 border-white bg-white rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500 text-base font-medium"
|
||||||
|
placeholder="تا مقدار..."
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-sm text-gray-500 font-medium">
|
||||||
|
{getSensorUnit(rule.sensorType)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -670,31 +746,16 @@ function AlertSettingsContent() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Preview */}
|
|
||||||
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border-2 border-blue-200 rounded-xl p-5">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<div className="flex-shrink-0 w-10 h-10 bg-blue-500 rounded-lg flex items-center justify-center">
|
|
||||||
<Bell className="w-5 h-5 text-white" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="text-xs font-medium text-blue-600 mb-1">پیشنمایش هشدار</div>
|
|
||||||
<div className="text-sm text-gray-800 leading-relaxed">
|
|
||||||
{generatePreviewText()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Active Status */}
|
{/* Active Status */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="isActive"
|
id="isEnabled"
|
||||||
checked={formData.isActive}
|
checked={formData.isEnabled}
|
||||||
onChange={(e) => setFormData({ ...formData, isActive: e.target.checked })}
|
onChange={(e) => setFormData({ ...formData, isEnabled: e.target.checked })}
|
||||||
className="w-4 h-4 text-orange-600 border-gray-300 rounded focus:ring-orange-500"
|
className="w-4 h-4 text-orange-600 border-gray-300 rounded focus:ring-orange-500"
|
||||||
/>
|
/>
|
||||||
<label htmlFor="isActive" className="text-sm font-medium text-gray-700">
|
<label htmlFor="isEnabled" className="text-sm font-medium text-gray-700">
|
||||||
هشدار فعال باشد
|
هشدار فعال باشد
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -717,6 +778,7 @@ function AlertSettingsContent() {
|
|||||||
{saving ? 'در حال ذخیره...' : editingAlert ? 'ذخیره تغییرات' : 'افزودن هشدار'}
|
{saving ? 'در حال ذخیره...' : editingAlert ? 'ذخیره تغییرات' : 'افزودن هشدار'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -93,19 +93,32 @@ export type DailyReportDto = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type AlertRuleDto = {
|
export type AlertRuleDto = {
|
||||||
id?: number
|
id: number
|
||||||
|
alertConditionId: number
|
||||||
sensorType: 0 | 1 | 2 | 3 | 4 // Temperature=0, Humidity=1, Soil=2, Gas=3, Lux=4
|
sensorType: 0 | 1 | 2 | 3 | 4 // Temperature=0, Humidity=1, Soil=2, Gas=3, Lux=4
|
||||||
comparisonType: 0 | 1 | 2 | 3 // GreaterThan=0, LessThan=1, Between=2, OutOfRange=3
|
comparisonType: 0 | 1 | 2 | 3 // GreaterThan=0, LessThan=1, Between=2, OutOfRange=3
|
||||||
threshold: number
|
value1: number
|
||||||
thresholdMax?: number // برای Between و OutOfRange
|
value2?: number // برای Between و OutOfRange
|
||||||
|
order: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreateAlertRuleRequest = {
|
||||||
|
sensorType: 0 | 1 | 2 | 3 | 4
|
||||||
|
comparisonType: 0 | 1 | 2 | 3
|
||||||
|
value1: number
|
||||||
|
value2?: number
|
||||||
|
order: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AlertConditionDto = {
|
export type AlertConditionDto = {
|
||||||
id: number
|
id: number
|
||||||
deviceId: number
|
deviceId: number
|
||||||
|
deviceName: string
|
||||||
notificationType: 0 | 1 // Call=0, SMS=1
|
notificationType: 0 | 1 // Call=0, SMS=1
|
||||||
timeType: 0 | 1 | 2 // Day=0, Night=1, Always=2
|
timeType: 0 | 1 | 2 // Day=0, Night=1, Always=2
|
||||||
isActive: boolean
|
callCooldownMinutes: number
|
||||||
|
smsCooldownMinutes: number
|
||||||
|
isEnabled: boolean
|
||||||
rules: AlertRuleDto[]
|
rules: AlertRuleDto[]
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
@@ -115,17 +128,20 @@ export type CreateAlertConditionDto = {
|
|||||||
deviceId: number
|
deviceId: number
|
||||||
notificationType: 0 | 1
|
notificationType: 0 | 1
|
||||||
timeType: 0 | 1 | 2
|
timeType: 0 | 1 | 2
|
||||||
isActive: boolean
|
callCooldownMinutes?: number
|
||||||
rules: AlertRuleDto[]
|
smsCooldownMinutes?: number
|
||||||
|
isEnabled: boolean
|
||||||
|
rules: CreateAlertRuleRequest[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UpdateAlertConditionDto = {
|
export type UpdateAlertConditionDto = {
|
||||||
id: number
|
id: number
|
||||||
deviceId: number
|
|
||||||
notificationType: 0 | 1
|
notificationType: 0 | 1
|
||||||
timeType: 0 | 1 | 2
|
timeType: 0 | 1 | 2
|
||||||
isActive: boolean
|
callCooldownMinutes?: number
|
||||||
rules: AlertRuleDto[]
|
smsCooldownMinutes?: number
|
||||||
|
isEnabled: boolean
|
||||||
|
rules: CreateAlertRuleRequest[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? 'https://ghback.nabaksoft.ir'
|
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? 'https://ghback.nabaksoft.ir'
|
||||||
@@ -196,9 +212,8 @@ export const api = {
|
|||||||
http<DailyReportDto>(`${API_BASE}/api/DailyReport?deviceId=${deviceId}&persianDate=${encodeURIComponent(persianDate)}`),
|
http<DailyReportDto>(`${API_BASE}/api/DailyReport?deviceId=${deviceId}&persianDate=${encodeURIComponent(persianDate)}`),
|
||||||
|
|
||||||
// Alert Conditions
|
// Alert Conditions
|
||||||
getAlertConditions: (deviceId?: number) => {
|
getAlertConditions: (deviceId: number) => {
|
||||||
const params = deviceId ? `?deviceId=${deviceId}` : ''
|
return http<AlertConditionDto[]>(`${API_BASE}/api/alertconditions/device/${deviceId}`)
|
||||||
return http<AlertConditionDto[]>(`${API_BASE}/api/alertconditions${params}`)
|
|
||||||
},
|
},
|
||||||
getAlertCondition: (id: number) =>
|
getAlertCondition: (id: number) =>
|
||||||
http<AlertConditionDto>(`${API_BASE}/api/alertconditions/${id}`),
|
http<AlertConditionDto>(`${API_BASE}/api/alertconditions/${id}`),
|
||||||
|
|||||||
Reference in New Issue
Block a user