fix alert-settings ui and add relay config
Some checks failed
Deploy MyApp on Same Server / build-and-deploy (push) Has been cancelled
Some checks failed
Deploy MyApp on Same Server / build-and-deploy (push) Has been cancelled
This commit is contained in:
@@ -1,69 +1,13 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import React, { useEffect, useState, useCallback, Suspense } from 'react'
|
import { useEffect, useState, useCallback, Suspense } from 'react'
|
||||||
import { useSearchParams } from 'next/navigation'
|
import { useSearchParams } from 'next/navigation'
|
||||||
import {
|
import { api, AlertConditionDto, UpdateAlertConditionDto, CreateAlertConditionDto, CreateAlertRuleRequest } from '@/lib/api'
|
||||||
api,
|
import { Bell, Plus, AlertTriangle } from 'lucide-react'
|
||||||
AlertConditionDto,
|
|
||||||
CreateAlertConditionDto,
|
|
||||||
UpdateAlertConditionDto,
|
|
||||||
AlertRuleDto,
|
|
||||||
CreateAlertRuleRequest
|
|
||||||
} from '@/lib/api'
|
|
||||||
import {
|
|
||||||
Bell,
|
|
||||||
Plus,
|
|
||||||
Pencil,
|
|
||||||
Trash2,
|
|
||||||
X,
|
|
||||||
AlertTriangle,
|
|
||||||
Thermometer,
|
|
||||||
Droplets,
|
|
||||||
Leaf,
|
|
||||||
Wind,
|
|
||||||
Sun,
|
|
||||||
Check,
|
|
||||||
Phone,
|
|
||||||
MessageSquare,
|
|
||||||
Moon,
|
|
||||||
Zap,
|
|
||||||
Maximize2,
|
|
||||||
ArrowLeftRight,
|
|
||||||
LucideIcon
|
|
||||||
} from 'lucide-react'
|
|
||||||
import Loading from '@/components/Loading'
|
import Loading from '@/components/Loading'
|
||||||
import { PageHeader, EmptyState, Modal, IconButton, Badge } from '@/components/common'
|
import { PageHeader, EmptyState } from '@/components/common'
|
||||||
import { confirmDialog } from '@/components/utils'
|
import { confirmDialog } from '@/components/utils'
|
||||||
|
import { AlertListItem } from '@/components/alert-settings/AlertListItem'
|
||||||
type SensorType = 0 | 1 | 2 | 3 | 4
|
import { AlertFormModal } from '@/components/alert-settings/AlertFormModal'
|
||||||
type ComparisonType = 0 | 1 | 2 | 3
|
|
||||||
type NotificationType = 0 | 1
|
|
||||||
type TimeType = 0 | 1 | 2
|
|
||||||
|
|
||||||
const SENSOR_TYPES: { value: SensorType; label: string; icon: LucideIcon; unit: string }[] = [
|
|
||||||
{ value: 0, label: 'دما', icon: Thermometer, unit: '°C' },
|
|
||||||
{ value: 1, label: 'رطوبت هوا', icon: Droplets, unit: '%' },
|
|
||||||
{ value: 2, label: 'رطوبت خاک', icon: Leaf, unit: '%' },
|
|
||||||
{ value: 3, label: 'گاز', icon: Wind, unit: 'PPM' },
|
|
||||||
{ value: 4, label: 'نور', icon: Sun, unit: 'Lux' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const COMPARISON_TYPES: { value: ComparisonType; label: string; icon: LucideIcon | (() => React.JSX.Element); needsMax: boolean }[] = [
|
|
||||||
{ value: 0, label: 'بزرگتر از', icon: () => <span className="text-2xl font-bold">></span>, needsMax: false },
|
|
||||||
{ value: 1, label: 'کوچکتر از', icon: () => <span className="text-2xl font-bold"><</span>, needsMax: false },
|
|
||||||
{ value: 2, label: 'بین', icon: ArrowLeftRight, needsMax: true },
|
|
||||||
{ value: 3, label: 'خارج از محدوده', icon: Maximize2, needsMax: true }
|
|
||||||
]
|
|
||||||
|
|
||||||
const NOTIFICATION_TYPES: { value: NotificationType; label: string; icon: LucideIcon }[] = [
|
|
||||||
{ value: 0, label: 'تماس تلفنی', icon: Phone },
|
|
||||||
{ value: 1, label: 'پیامک', icon: MessageSquare }
|
|
||||||
]
|
|
||||||
|
|
||||||
const TIME_TYPES: { value: TimeType; label: string; icon: LucideIcon; description: string }[] = [
|
|
||||||
{ value: 0, label: 'روز', icon: Sun, description: 'فقط در روز بررسی شود' },
|
|
||||||
{ value: 1, label: 'شب', icon: Moon, description: 'فقط در شب بررسی شود' },
|
|
||||||
{ value: 2, label: 'همیشه', icon: Zap, description: 'در هر زمان بررسی شود' }
|
|
||||||
]
|
|
||||||
|
|
||||||
function AlertSettingsContent() {
|
function AlertSettingsContent() {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
@@ -71,691 +15,139 @@ function AlertSettingsContent() {
|
|||||||
|
|
||||||
const [alerts, setAlerts] = useState<AlertConditionDto[]>([])
|
const [alerts, setAlerts] = useState<AlertConditionDto[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [showModal, setShowModal] = useState(false)
|
const [modal, setModal] = useState<{ open: boolean; editing: AlertConditionDto | null }>({ open: false, editing: null })
|
||||||
const [editingAlert, setEditingAlert] = useState<AlertConditionDto | null>(null)
|
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
|
|
||||||
// Form state
|
|
||||||
const [formData, setFormData] = useState<CreateAlertConditionDto>({
|
const [formData, setFormData] = useState<CreateAlertConditionDto>({
|
||||||
deviceId: deviceId,
|
deviceId,
|
||||||
notificationType: 0,
|
notificationType: 0,
|
||||||
timeType: 2,
|
timeType: 2,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
rules: []
|
rules: [{ sensorType: 0, comparisonType: 0, value1: 30, order: 0 }]
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadAlerts = useCallback(async () => {
|
const loadAlerts = useCallback(async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try { setAlerts(await api.getAlertConditions(deviceId)) }
|
||||||
const data = await api.getAlertConditions(deviceId)
|
catch (e) { console.error('Error loading alerts:', e) }
|
||||||
setAlerts(data)
|
finally { setLoading(false) }
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading alerts:', error)
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}, [deviceId])
|
}, [deviceId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { loadAlerts() }, [loadAlerts])
|
||||||
loadAlerts()
|
|
||||||
}, [loadAlerts])
|
|
||||||
|
|
||||||
const openCreateModal = () => {
|
const openCreate = () => {
|
||||||
setEditingAlert(null)
|
setModal({ open: true, editing: null })
|
||||||
setFormData({
|
setFormData({
|
||||||
deviceId: deviceId,
|
deviceId,
|
||||||
notificationType: 0,
|
notificationType: 0,
|
||||||
timeType: 2,
|
timeType: 2,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
rules: [{
|
rules: [{ sensorType: 0, comparisonType: 0, value1: 30, order: 0 }]
|
||||||
sensorType: 0,
|
|
||||||
comparisonType: 0,
|
|
||||||
value1: 30,
|
|
||||||
order: 0
|
|
||||||
}]
|
|
||||||
})
|
})
|
||||||
setShowModal(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const openEditModal = (alert: AlertConditionDto) => {
|
const openEdit = (alert: AlertConditionDto) => {
|
||||||
setEditingAlert(alert)
|
setModal({ open: true, editing: alert })
|
||||||
setFormData({
|
setFormData({
|
||||||
notificationType: alert.notificationType,
|
notificationType: alert.notificationType,
|
||||||
timeType: alert.timeType,
|
timeType: alert.timeType,
|
||||||
isEnabled: alert.isEnabled,
|
isEnabled: alert.isEnabled,
|
||||||
deviceId: alert.deviceId,
|
deviceId: alert.deviceId,
|
||||||
rules: alert.rules.map((r, idx) => ({
|
rules: alert.rules.map((r, i) => ({
|
||||||
sensorType: r.sensorType,
|
sensorType: r.sensorType, comparisonType: r.comparisonType,
|
||||||
comparisonType: r.comparisonType,
|
value1: r.value1, value2: r.value2, order: i
|
||||||
value1: r.value1,
|
|
||||||
value2: r.value2,
|
|
||||||
order: idx
|
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
setShowModal(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => setModal({ open: false, editing: null })
|
||||||
setShowModal(false)
|
|
||||||
setEditingAlert(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
if (!formData.rules.length) { window.alert('لطفاً حداقل یک قانون اضافه کنید'); return }
|
||||||
if (formData.rules.length === 0) {
|
|
||||||
window.alert('لطفاً حداقل یک قانون اضافه کنید')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (editingAlert) {
|
if (modal.editing) await api.updateAlertCondition({ id: modal.editing.id, ...formData })
|
||||||
// Update
|
else await api.createAlertCondition(formData)
|
||||||
const updateDto: UpdateAlertConditionDto = {
|
|
||||||
id: editingAlert.id,
|
|
||||||
...formData
|
|
||||||
}
|
|
||||||
await api.updateAlertCondition(updateDto)
|
|
||||||
} else {
|
|
||||||
// Create
|
|
||||||
await api.createAlertCondition(formData)
|
|
||||||
}
|
|
||||||
|
|
||||||
await loadAlerts()
|
await loadAlerts()
|
||||||
closeModal()
|
closeModal()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving alert:', error)
|
console.error('Error saving alert:', error)
|
||||||
window.alert('خطا در ذخیره هشدار. لطفاً دوباره تلاش کنید.')
|
window.alert('خطا در ذخیره هشدار. لطفاً دوباره تلاش کنید.')
|
||||||
} finally {
|
} finally { setSaving(false) }
|
||||||
setSaving(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = async (id: number) => {
|
const handleDelete = async (id: number) => {
|
||||||
const confirmed = await confirmDialog({
|
if (!await confirmDialog({ message: 'آیا از حذف این هشدار اطمینان دارید؟', variant: 'danger', confirmText: 'حذف', cancelText: 'انصراف' })) return
|
||||||
message: 'آیا از حذف این هشدار اطمینان دارید؟',
|
try { await api.deleteAlertCondition(id); await loadAlerts() }
|
||||||
variant: 'danger',
|
catch (e) { console.error('Error deleting alert:', e); window.alert('خطا در حذف هشدار.') }
|
||||||
confirmText: 'حذف',
|
|
||||||
cancelText: 'انصراف'
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!confirmed) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.deleteAlertCondition(id)
|
|
||||||
await loadAlerts()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting alert:', error)
|
|
||||||
window.alert('خطا در حذف هشدار. لطفاً دوباره تلاش کنید.')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleActive = async (alert: AlertConditionDto) => {
|
const toggleActive = async (alert: AlertConditionDto) => {
|
||||||
try {
|
try {
|
||||||
const updateDto: UpdateAlertConditionDto = {
|
await api.updateAlertCondition({
|
||||||
id: alert.id,
|
id: alert.id, notificationType: alert.notificationType, timeType: alert.timeType,
|
||||||
notificationType: alert.notificationType,
|
|
||||||
timeType: alert.timeType,
|
|
||||||
isEnabled: !alert.isEnabled,
|
isEnabled: !alert.isEnabled,
|
||||||
rules: alert.rules.map((r, idx) => ({
|
rules: alert.rules.map((r, i) => ({ sensorType: r.sensorType, comparisonType: r.comparisonType, value1: r.value1, value2: r.value2, order: i }))
|
||||||
sensorType: r.sensorType,
|
})
|
||||||
comparisonType: r.comparisonType,
|
|
||||||
value1: r.value1,
|
|
||||||
value2: r.value2,
|
|
||||||
order: idx
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
await api.updateAlertCondition(updateDto)
|
|
||||||
await loadAlerts()
|
await loadAlerts()
|
||||||
} catch (error) {
|
} catch (e) { console.error('Error toggling alert:', e); window.alert('خطا در تغییر وضعیت هشدار.') }
|
||||||
console.error('Error toggling alert:', error)
|
|
||||||
window.alert('خطا در تغییر وضعیت هشدار.')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const addRule = () => {
|
const addRule = () => setFormData(f => ({ ...f, rules: [...f.rules, { sensorType: 0, comparisonType: 0, value1: 30, order: f.rules.length }] }))
|
||||||
setFormData({
|
const removeRule = (index: number) => setFormData(f => ({ ...f, rules: f.rules.filter((_, i) => i !== index).map((r, i) => ({ ...r, order: i })) }))
|
||||||
...formData,
|
const updateRule = (index: number, updates: Partial<CreateAlertRuleRequest>) => setFormData(f => {
|
||||||
rules: [
|
const rules = [...f.rules]; rules[index] = { ...rules[index], ...updates }; return { ...f, rules }
|
||||||
...formData.rules,
|
|
||||||
{
|
|
||||||
sensorType: 0,
|
|
||||||
comparisonType: 0,
|
|
||||||
value1: 30,
|
|
||||||
order: formData.rules.length
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeRule = (index: number) => {
|
|
||||||
const newRules = formData.rules
|
|
||||||
.filter((_, i) => i !== index)
|
|
||||||
.map((r, idx) => ({ ...r, order: idx }))
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
rules: newRules
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateRule = (index: number, updates: Partial<CreateAlertRuleRequest>) => {
|
|
||||||
const newRules = [...formData.rules]
|
|
||||||
newRules[index] = { ...newRules[index], ...updates }
|
|
||||||
setFormData({ ...formData, rules: newRules })
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSensorIcon = (type: SensorType): LucideIcon => {
|
|
||||||
const sensor = SENSOR_TYPES.find(s => s.value === type)
|
|
||||||
return sensor ? sensor.icon : AlertTriangle
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSensorLabel = (type: SensorType) => {
|
|
||||||
const sensor = SENSOR_TYPES.find(s => s.value === type)
|
|
||||||
return sensor?.label ?? type.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSensorUnit = (type: SensorType) => {
|
|
||||||
const sensor = SENSOR_TYPES.find(s => s.value === type)
|
|
||||||
return sensor?.unit ?? ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const getComparisonLabel = (type: ComparisonType) => {
|
|
||||||
const comp = COMPARISON_TYPES.find(c => c.value === type)
|
|
||||||
return comp?.label ?? type.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
const getNotificationLabel = (type: NotificationType) => {
|
|
||||||
const notif = NOTIFICATION_TYPES.find(n => n.value === type)
|
|
||||||
return notif?.label ?? type.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTimeTypeLabel = (type: TimeType) => {
|
|
||||||
const time = TIME_TYPES.find(t => t.value === type)
|
|
||||||
return time?.label ?? type.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatRuleText = (rule: AlertRuleDto) => {
|
|
||||||
const comp = COMPARISON_TYPES.find(c => c.value === rule.comparisonType)
|
|
||||||
if (comp?.needsMax) {
|
|
||||||
return `${getComparisonLabel(rule.comparisonType)}: ${rule.value1} - ${rule.value2 ?? '?'} ${getSensorUnit(rule.sensorType)}`
|
|
||||||
}
|
|
||||||
const symbol = rule.comparisonType === 0 ? '>' : rule.comparisonType === 1 ? '<' : '?'
|
|
||||||
return `${symbol} ${rule.value1} ${getSensorUnit(rule.sensorType)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const generatePreviewText = () => {
|
|
||||||
if (formData.rules.length === 0) {
|
|
||||||
return '🤔 هنوز شرطی تعریف نکردی!'
|
|
||||||
}
|
|
||||||
|
|
||||||
// نوع اطلاعرسانی
|
|
||||||
const notifText = formData.notificationType === 0 ? 'بهت زنگ بزنم' : 'برات پیامک بفرستم'
|
|
||||||
|
|
||||||
// زمان
|
|
||||||
const timeText = formData.timeType === 0
|
|
||||||
? 'فقط توی روز'
|
|
||||||
: formData.timeType === 1
|
|
||||||
? 'فقط توی شب'
|
|
||||||
: ''
|
|
||||||
|
|
||||||
// قوانین
|
|
||||||
const rulesText = formData.rules.map(rule => {
|
|
||||||
const sensorName = getSensorLabel(rule.sensorType)
|
|
||||||
const unit = getSensorUnit(rule.sensorType)
|
|
||||||
const comp = COMPARISON_TYPES.find(c => c.value === rule.comparisonType)
|
|
||||||
|
|
||||||
if (comp?.needsMax) {
|
|
||||||
// بین یا خارج از محدوده
|
|
||||||
if (rule.comparisonType === 2) {
|
|
||||||
return `${sensorName} بین ${rule.value1} تا ${rule.value2 ?? '؟'} ${unit} باشه`
|
|
||||||
} else {
|
|
||||||
return `${sensorName} از بازه ${rule.value1} تا ${rule.value2 ?? '؟'} ${unit} خارج شه`
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// بزرگتر یا کوچکتر
|
|
||||||
const compText = rule.comparisonType === 0 ? 'از' : 'از'
|
|
||||||
const direction = rule.comparisonType === 0 ? 'بالاتر' : 'پایینتر'
|
|
||||||
return `${sensorName} ${direction} ${compText} ${rule.value1} ${unit} بره`
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// ساخت جمله نهایی
|
if (loading) return <Loading message="در حال بارگذاری تنظیمات هشدار..." />
|
||||||
let baseText = ''
|
|
||||||
if (rulesText.length === 1) {
|
|
||||||
baseText = `وقتی ${rulesText[0]}`
|
|
||||||
} else {
|
|
||||||
const lastRule = rulesText[rulesText.length - 1]
|
|
||||||
const otherRules = rulesText.slice(0, -1).join(' و ')
|
|
||||||
baseText = `وقتی ${otherRules} و ${lastRule}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// اضافه کردن زمان اگر مشخص شده
|
|
||||||
const timePrefix = timeText ? `${timeText}، ` : ''
|
|
||||||
|
|
||||||
return `${timePrefix}${baseText}، ${notifText} 📱`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <Loading message="در حال بارگذاری تنظیمات هشدار..." />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen p-4 md:p-6 bg-gray-50">
|
<div className="min-h-screen p-4 md:p-6 bg-gray-50">
|
||||||
<div className="max-w-7xl mx-auto space-y-6">
|
<div className="max-w-7xl mx-auto space-y-6">
|
||||||
{/* Header */}
|
|
||||||
<PageHeader
|
<PageHeader
|
||||||
icon={Bell}
|
icon={Bell}
|
||||||
title="تنظیمات هشدارها"
|
title="تنظیمات هشدارها"
|
||||||
subtitle="مدیریت شرایط و هشدارهای دستگاه"
|
subtitle="مدیریت شرایط و هشدارهای دستگاه"
|
||||||
iconGradient="from-orange-500 to-red-600"
|
iconGradient="from-orange-500 to-red-600"
|
||||||
action={
|
action={
|
||||||
<button
|
<button onClick={openCreate}
|
||||||
onClick={openCreateModal}
|
className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-orange-500 to-red-600 hover:from-orange-600 hover:to-red-700 text-white rounded-xl transition-all duration-200 font-medium shadow-md hover:shadow-lg">
|
||||||
className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-orange-500 to-red-600 hover:from-orange-600 hover:to-red-700 text-white rounded-xl transition-all duration-200 font-medium shadow-md hover:shadow-lg"
|
<Plus className="w-5 h-5" /> افزودن هشدار
|
||||||
>
|
|
||||||
<Plus className="w-5 h-5" />
|
|
||||||
افزودن هشدار
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Alerts List */}
|
|
||||||
<div className="bg-white rounded-2xl shadow-md border border-gray-200 overflow-hidden">
|
<div className="bg-white rounded-2xl shadow-md border border-gray-200 overflow-hidden">
|
||||||
<div className="p-6 border-b border-gray-200">
|
<div className="p-6 border-b border-gray-200">
|
||||||
<h2 className="text-xl font-bold text-gray-900">
|
<h2 className="text-xl font-bold text-gray-900">لیست هشدارها ({alerts.length})</h2>
|
||||||
لیست هشدارها ({alerts.length})
|
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{alerts.length === 0 ? (
|
{alerts.length === 0 ? (
|
||||||
<EmptyState
|
<EmptyState icon={AlertTriangle} message="هیچ هشداری ثبت نشده است"
|
||||||
icon={AlertTriangle}
|
action={<button onClick={openCreate} className="inline-flex items-center gap-2 px-4 py-2 bg-orange-500 hover:bg-orange-600 text-white rounded-lg transition-colors"><Plus className="w-4 h-4" /> افزودن اولین هشدار</button>}
|
||||||
message="هیچ هشداری ثبت نشده است"
|
|
||||||
action={
|
|
||||||
<button
|
|
||||||
onClick={openCreateModal}
|
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 bg-orange-500 hover:bg-orange-600 text-white rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<Plus className="w-4 h-4" />
|
|
||||||
افزودن اولین هشدار
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<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.isEnabled ? 'opacity-50' : ''}`}>
|
<AlertListItem key={alert.id} alert={alert} onEdit={openEdit} onDelete={handleDelete} onToggleActive={toggleActive} />
|
||||||
<div className="flex items-start justify-between gap-4">
|
|
||||||
<div className="flex-1 space-y-3">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center gap-3 flex-wrap">
|
|
||||||
<Badge
|
|
||||||
variant="warning"
|
|
||||||
icon={NOTIFICATION_TYPES.find(n => n.value === alert.notificationType)?.icon}
|
|
||||||
>
|
|
||||||
{getNotificationLabel(alert.notificationType)}
|
|
||||||
</Badge>
|
|
||||||
<Badge
|
|
||||||
variant="info"
|
|
||||||
icon={TIME_TYPES.find(t => t.value === alert.timeType)?.icon}
|
|
||||||
>
|
|
||||||
{getTimeTypeLabel(alert.timeType)}
|
|
||||||
</Badge>
|
|
||||||
<button
|
|
||||||
onClick={() => toggleActive(alert)}
|
|
||||||
className={`inline-flex items-center gap-1 px-3 py-1 rounded-full text-sm font-medium transition-colors ${
|
|
||||||
alert.isEnabled
|
|
||||||
? 'bg-green-100 text-green-800 hover:bg-green-200'
|
|
||||||
: 'bg-gray-100 text-gray-800 hover:bg-gray-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{alert.isEnabled ? (
|
|
||||||
<>
|
|
||||||
<Check className="w-3 h-3" />
|
|
||||||
فعال
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<X className="w-3 h-3" />
|
|
||||||
غیرفعال
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Rules */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-sm font-medium text-gray-700">شرایط:</div>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{alert.rules.map((rule, idx) => {
|
|
||||||
const Icon = getSensorIcon(rule.sensorType)
|
|
||||||
return (
|
|
||||||
<div key={idx} className="flex items-center gap-2 px-3 py-2 bg-gray-100 rounded-lg text-sm">
|
|
||||||
<Icon className="w-4 h-4 text-gray-600" />
|
|
||||||
<span className="font-medium">{getSensorLabel(rule.sensorType)}</span>
|
|
||||||
<span className="font-mono text-gray-700">{formatRuleText(rule)}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<IconButton
|
|
||||||
icon={Pencil}
|
|
||||||
variant="primary"
|
|
||||||
onClick={() => openEditModal(alert)}
|
|
||||||
title="ویرایش"
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
icon={Trash2}
|
|
||||||
variant="danger"
|
|
||||||
onClick={() => handleDelete(alert.id)}
|
|
||||||
title="حذف"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal */}
|
<AlertFormModal
|
||||||
<Modal
|
isOpen={modal.open}
|
||||||
isOpen={showModal}
|
editingAlert={!!modal.editing}
|
||||||
|
saving={saving}
|
||||||
|
formData={formData}
|
||||||
onClose={closeModal}
|
onClose={closeModal}
|
||||||
title={editingAlert ? 'ویرایش هشدار' : 'افزودن هشدار جدید'}
|
onSubmit={handleSubmit}
|
||||||
size="xl"
|
onFormDataChange={setFormData}
|
||||||
>
|
onAddRule={addRule}
|
||||||
|
onRemoveRule={removeRule}
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
onUpdateRule={updateRule}
|
||||||
{/* Preview - Sticky at top */}
|
|
||||||
<div className="sticky top-[73px] z-10 bg-gradient-to-r from-blue-500 to-indigo-600 shadow-lg -mx-6 -mt-6">
|
|
||||||
<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="px-6 space-y-6">
|
|
||||||
{/* Notification Type */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-3">
|
|
||||||
نوع اطلاعرسانی
|
|
||||||
</label>
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
{NOTIFICATION_TYPES.map(notif => {
|
|
||||||
const Icon = notif.icon
|
|
||||||
const isSelected = formData.notificationType === notif.value
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={notif.value}
|
|
||||||
type="button"
|
|
||||||
onClick={() => setFormData({ ...formData, notificationType: notif.value })}
|
|
||||||
className={`flex items-center gap-3 p-4 rounded-lg border-2 transition-all ${
|
|
||||||
isSelected
|
|
||||||
? 'border-orange-500 bg-orange-50 text-orange-700'
|
|
||||||
: 'border-gray-200 hover:border-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Icon className={`w-6 h-6 ${isSelected ? 'text-orange-500' : 'text-gray-400'}`} />
|
|
||||||
<span className="text-sm font-medium">{notif.label}</span>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Time Type */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
زمان بررسی شرایط
|
|
||||||
</label>
|
|
||||||
<p className="text-xs text-gray-500 mb-3">
|
|
||||||
مشخص کنید شرایط در چه زمانی بررسی و هشدار ارسال شود
|
|
||||||
</p>
|
|
||||||
<div className="grid grid-cols-3 gap-3">
|
|
||||||
{TIME_TYPES.map(time => {
|
|
||||||
const Icon = time.icon
|
|
||||||
const isSelected = formData.timeType === time.value
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={time.value}
|
|
||||||
type="button"
|
|
||||||
onClick={() => setFormData({ ...formData, timeType: time.value })}
|
|
||||||
className={`flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-all ${
|
|
||||||
isSelected
|
|
||||||
? 'border-blue-500 bg-blue-50 text-blue-700'
|
|
||||||
: 'border-gray-200 hover:border-gray-300'
|
|
||||||
}`}
|
|
||||||
title={time.description}
|
|
||||||
>
|
|
||||||
<Icon className={`w-6 h-6 ${isSelected ? 'text-blue-500' : 'text-gray-400'}`} />
|
|
||||||
<span className="text-sm font-medium">{time.label}</span>
|
|
||||||
<span className="text-xs text-gray-500">{time.description}</span>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Rules */}
|
|
||||||
<div className="border-t border-gray-200 pt-6">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-gray-700 block">
|
|
||||||
شرایط هشدار
|
|
||||||
</label>
|
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
|
||||||
{formData.rules.length === 0
|
|
||||||
? 'هیچ شرطی تعریف نشده است'
|
|
||||||
: formData.rules.length === 1
|
|
||||||
? 'یک شرط تعریف شده است'
|
|
||||||
: `${formData.rules.length} شرط تعریف شده است (همه باید برقرار باشند)`
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={addRule}
|
|
||||||
className="flex items-center gap-2 px-3 py-1.5 bg-green-500 hover:bg-green-600 text-white rounded-lg transition-colors text-sm"
|
|
||||||
>
|
|
||||||
<Plus className="w-4 h-4" />
|
|
||||||
افزودن شرط
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
{formData.rules.map((rule, index) => {
|
|
||||||
const needsMax = COMPARISON_TYPES.find(c => c.value === rule.comparisonType)?.needsMax
|
|
||||||
|
|
||||||
return (
|
|
||||||
<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-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-xs font-bold text-orange-700 bg-white px-2.5 py-1 rounded-full shadow-sm">
|
|
||||||
{formData.rules.length === 1 ? '⚡ شرط' : `⚡ شرط ${index + 1}`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{formData.rules.length > 1 && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => removeRule(index)}
|
|
||||||
className="p-2 text-red-600 hover:bg-white/50 rounded-lg transition-colors"
|
|
||||||
title="حذف این شرط"
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sensor Type - Dropdown Style */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
🎯 کدام سنسور را بررسی کنیم؟
|
|
||||||
</label>
|
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-2">
|
|
||||||
{SENSOR_TYPES.map(sensor => {
|
|
||||||
const Icon = sensor.icon
|
|
||||||
const isSelected = rule.sensorType === sensor.value
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={sensor.value}
|
|
||||||
type="button"
|
|
||||||
onClick={() => updateRule(index, { sensorType: sensor.value })}
|
|
||||||
className={`flex flex-col items-center gap-2 p-3 rounded-lg border-2 transition-all ${
|
|
||||||
isSelected
|
|
||||||
? 'border-orange-500 bg-white shadow-md scale-105'
|
|
||||||
: 'border-white/50 bg-white/50 hover:border-orange-300 hover:bg-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Icon className={`w-6 h-6 ${isSelected ? 'text-orange-500' : 'text-gray-400'}`} />
|
|
||||||
<span className={`text-xs font-medium ${isSelected ? 'text-orange-700' : 'text-gray-600'}`}>
|
|
||||||
{sensor.label}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Comparison Type - Card Style */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
📊 چه موقع هشدار بدهیم؟
|
|
||||||
</label>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
|
||||||
{COMPARISON_TYPES.map(comp => {
|
|
||||||
const Icon = comp.icon
|
|
||||||
const isSelected = rule.comparisonType === comp.value
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={comp.value}
|
|
||||||
type="button"
|
|
||||||
onClick={() => updateRule(index, { comparisonType: comp.value })}
|
|
||||||
className={`flex flex-col items-center gap-2 p-3 rounded-lg border-2 transition-all ${
|
|
||||||
isSelected
|
|
||||||
? 'border-orange-500 bg-white shadow-md scale-105'
|
|
||||||
: 'border-white/50 bg-white/50 hover:border-orange-300 hover:bg-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{'length' in Icon && Icon.length === 0 ? (
|
|
||||||
<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>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Threshold Values */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
🔢 مقدار آستانه
|
|
||||||
</label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
value={rule.value1}
|
|
||||||
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"
|
|
||||||
placeholder={needsMax ? 'از مقدار...' : 'مقدار...'}
|
|
||||||
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>
|
|
||||||
{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>
|
|
||||||
|
|
||||||
{/* Active Status */}
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="isEnabled"
|
|
||||||
checked={formData.isEnabled}
|
|
||||||
onChange={(e) => setFormData({ ...formData, isEnabled: e.target.checked })}
|
|
||||||
className="w-4 h-4 text-orange-600 border-gray-300 rounded focus:ring-orange-500"
|
|
||||||
/>
|
|
||||||
<label htmlFor="isEnabled" className="text-sm font-medium text-gray-700">
|
|
||||||
هشدار فعال باشد
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Modal Footer */}
|
|
||||||
<div className="flex items-center justify-end gap-3 pt-4 border-t border-gray-200 px-6">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={closeModal}
|
|
||||||
className="px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-lg transition-colors"
|
|
||||||
disabled={saving}
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="px-6 py-2 bg-gradient-to-r from-orange-500 to-red-600 hover:from-orange-600 hover:to-red-700 text-white rounded-lg transition-all duration-200 font-medium shadow-md hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
disabled={saving}
|
|
||||||
>
|
|
||||||
{saving ? 'در حال ذخیره...' : editingAlert ? 'ذخیره تغییرات' : 'افزودن هشدار'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
138
src/components/alert-settings/AlertFormModal.tsx
Normal file
138
src/components/alert-settings/AlertFormModal.tsx
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Plus } from 'lucide-react'
|
||||||
|
import { CreateAlertConditionDto, CreateAlertRuleRequest } from '@/lib/api'
|
||||||
|
import { Modal } from '@/components/common'
|
||||||
|
import { AlertPreview } from './AlertPreview'
|
||||||
|
import { NotificationTypeSelector } from './NotificationTypeSelector'
|
||||||
|
import { TimeTypeSelector } from './TimeTypeSelector'
|
||||||
|
import { RuleFormCard } from './RuleFormCard'
|
||||||
|
|
||||||
|
type AlertFormModalProps = {
|
||||||
|
isOpen: boolean
|
||||||
|
editingAlert: boolean
|
||||||
|
saving: boolean
|
||||||
|
formData: CreateAlertConditionDto
|
||||||
|
onClose: () => void
|
||||||
|
onSubmit: (e: React.FormEvent) => void
|
||||||
|
onFormDataChange: (data: CreateAlertConditionDto) => void
|
||||||
|
onAddRule: () => void
|
||||||
|
onRemoveRule: (index: number) => void
|
||||||
|
onUpdateRule: (index: number, updates: Partial<CreateAlertRuleRequest>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AlertFormModal({
|
||||||
|
isOpen,
|
||||||
|
editingAlert,
|
||||||
|
saving,
|
||||||
|
formData,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
onFormDataChange,
|
||||||
|
onAddRule,
|
||||||
|
onRemoveRule,
|
||||||
|
onUpdateRule
|
||||||
|
}: AlertFormModalProps) {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
title={editingAlert ? 'ویرایش هشدار' : 'افزودن هشدار جدید'}
|
||||||
|
size="xl"
|
||||||
|
className="p-0"
|
||||||
|
>
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<AlertPreview
|
||||||
|
rules={formData.rules}
|
||||||
|
notificationType={formData.notificationType}
|
||||||
|
timeType={formData.timeType}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
<NotificationTypeSelector
|
||||||
|
value={formData.notificationType}
|
||||||
|
onChange={(value) => onFormDataChange({ ...formData, notificationType: value })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TimeTypeSelector
|
||||||
|
value={formData.timeType}
|
||||||
|
onChange={(value) => onFormDataChange({ ...formData, timeType: value })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Rules */}
|
||||||
|
<div className="border-t border-gray-100 pt-4">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-gray-500 block">
|
||||||
|
شرایط هشدار
|
||||||
|
</label>
|
||||||
|
<p className="text-[10px] text-gray-400 mt-0.5">
|
||||||
|
{formData.rules.length === 0
|
||||||
|
? 'هیچ شرطی تعریف نشده است'
|
||||||
|
: formData.rules.length === 1
|
||||||
|
? 'یک شرط تعریف شده است'
|
||||||
|
: `${formData.rules.length} شرط تعریف شده است`
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onAddRule}
|
||||||
|
className="flex items-center gap-1 px-2.5 py-1.5 bg-green-500 hover:bg-green-600 text-white rounded-lg transition-colors text-xs"
|
||||||
|
>
|
||||||
|
<Plus className="w-3.5 h-3.5" />
|
||||||
|
افزودن شرط
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{formData.rules.map((rule, index) => (
|
||||||
|
<RuleFormCard
|
||||||
|
key={index}
|
||||||
|
rule={rule}
|
||||||
|
index={index}
|
||||||
|
totalRules={formData.rules.length}
|
||||||
|
onUpdate={onUpdateRule}
|
||||||
|
onRemove={onRemoveRule}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Active Status */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="isEnabled"
|
||||||
|
checked={formData.isEnabled}
|
||||||
|
onChange={(e) => onFormDataChange({ ...formData, isEnabled: e.target.checked })}
|
||||||
|
className="w-3.5 h-3.5 text-orange-600 border-gray-300 rounded focus:ring-orange-500"
|
||||||
|
/>
|
||||||
|
<label htmlFor="isEnabled" className="text-xs font-medium text-gray-600">
|
||||||
|
هشدار فعال باشد
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Footer */}
|
||||||
|
<div className="flex items-center justify-end gap-2 pt-3 border-t border-gray-100">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-3 py-1.5 text-xs text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-4 py-1.5 text-xs bg-gradient-to-r from-orange-500 to-red-600 hover:from-orange-600 hover:to-red-700 text-white rounded-lg transition-all duration-200 font-medium shadow-sm hover:shadow disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
{saving ? 'در حال ذخیره...' : editingAlert ? 'ذخیره تغییرات' : 'افزودن هشدار'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
77
src/components/alert-settings/AlertListItem.tsx
Normal file
77
src/components/alert-settings/AlertListItem.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Pencil, Trash2, Check, X } from 'lucide-react'
|
||||||
|
import { AlertConditionDto } from '@/lib/api'
|
||||||
|
import { IconButton, Badge } from '@/components/common'
|
||||||
|
import { NOTIFICATION_TYPES, TIME_TYPES } from './constants'
|
||||||
|
import { getSensorIcon, getSensorLabel, getNotificationLabel, getTimeTypeLabel, formatRuleText } from './utils'
|
||||||
|
|
||||||
|
type AlertListItemProps = {
|
||||||
|
alert: AlertConditionDto
|
||||||
|
onEdit: (alert: AlertConditionDto) => void
|
||||||
|
onDelete: (id: number) => void
|
||||||
|
onToggleActive: (alert: AlertConditionDto) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AlertListItem({ alert, onEdit, onDelete, onToggleActive }: AlertListItemProps) {
|
||||||
|
return (
|
||||||
|
<div 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-1 space-y-3">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
|
<Badge
|
||||||
|
variant="warning"
|
||||||
|
icon={NOTIFICATION_TYPES.find(n => n.value === alert.notificationType)?.icon}
|
||||||
|
>
|
||||||
|
{getNotificationLabel(alert.notificationType)}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
variant="info"
|
||||||
|
icon={TIME_TYPES.find(t => t.value === alert.timeType)?.icon}
|
||||||
|
>
|
||||||
|
{getTimeTypeLabel(alert.timeType)}
|
||||||
|
</Badge>
|
||||||
|
<button
|
||||||
|
onClick={() => onToggleActive(alert)}
|
||||||
|
className={`inline-flex items-center gap-1 px-3 py-1 rounded-full text-sm font-medium transition-colors ${
|
||||||
|
alert.isEnabled
|
||||||
|
? 'bg-green-100 text-green-800 hover:bg-green-200'
|
||||||
|
: 'bg-gray-100 text-gray-800 hover:bg-gray-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{alert.isEnabled ? (
|
||||||
|
<><Check className="w-3 h-3" />فعال</>
|
||||||
|
) : (
|
||||||
|
<><X className="w-3 h-3" />غیرفعال</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Rules */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-sm font-medium text-gray-700">شرایط:</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{alert.rules.map((rule, idx) => {
|
||||||
|
const Icon = getSensorIcon(rule.sensorType)
|
||||||
|
return (
|
||||||
|
<div key={idx} className="flex items-center gap-2 px-3 py-2 bg-gray-100 rounded-lg text-sm">
|
||||||
|
<Icon className="w-4 h-4 text-gray-600" />
|
||||||
|
<span className="font-medium">{getSensorLabel(rule.sensorType)}</span>
|
||||||
|
<span className="font-mono text-gray-700">{formatRuleText(rule)}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<IconButton icon={Pencil} variant="primary" onClick={() => onEdit(alert)} title="ویرایش" />
|
||||||
|
<IconButton icon={Trash2} variant="danger" onClick={() => onDelete(alert.id)} title="حذف" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
31
src/components/alert-settings/AlertPreview.tsx
Normal file
31
src/components/alert-settings/AlertPreview.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Bell } from 'lucide-react'
|
||||||
|
import { SensorType, ComparisonType, NotificationType, TimeType } from './constants'
|
||||||
|
import { generatePreviewText } from './utils'
|
||||||
|
|
||||||
|
type AlertPreviewProps = {
|
||||||
|
rules: { sensorType: SensorType; comparisonType: ComparisonType; value1: number; value2?: number | null }[]
|
||||||
|
notificationType: NotificationType
|
||||||
|
timeType: TimeType
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AlertPreview({ rules, notificationType, timeType }: AlertPreviewProps) {
|
||||||
|
return (
|
||||||
|
<div className="sticky top-[45px] z-10 bg-gradient-to-r from-blue-500 to-indigo-600 shadow-lg -mx-6 -mt-6">
|
||||||
|
<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(rules, notificationType, timeType)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
39
src/components/alert-settings/NotificationTypeSelector.tsx
Normal file
39
src/components/alert-settings/NotificationTypeSelector.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { NOTIFICATION_TYPES, NotificationType } from './constants'
|
||||||
|
|
||||||
|
type NotificationTypeSelectorProps = {
|
||||||
|
value: NotificationType
|
||||||
|
onChange: (value: NotificationType) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NotificationTypeSelector({ value, onChange }: NotificationTypeSelectorProps) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-gray-500 mb-2">
|
||||||
|
نوع اطلاعرسانی
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{NOTIFICATION_TYPES.map(notif => {
|
||||||
|
const Icon = notif.icon
|
||||||
|
const isSelected = value === notif.value
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={notif.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onChange(notif.value)}
|
||||||
|
className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg border transition-all text-xs ${
|
||||||
|
isSelected
|
||||||
|
? 'border-orange-500 bg-orange-50 text-orange-700 shadow-sm'
|
||||||
|
: 'border-gray-200 bg-white text-gray-600 hover:border-gray-300 hover:bg-gray-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon className={`w-3.5 h-3.5 ${isSelected ? 'text-orange-500' : 'text-gray-400'}`} />
|
||||||
|
<span className="font-medium">{notif.label}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
133
src/components/alert-settings/RuleFormCard.tsx
Normal file
133
src/components/alert-settings/RuleFormCard.tsx
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Trash2 } from 'lucide-react'
|
||||||
|
import { SENSOR_TYPES, COMPARISON_TYPES, SensorType, ComparisonType } from './constants'
|
||||||
|
import { getSensorIcon, getSensorLabel, getSensorUnit, getComparisonLabel } from './utils'
|
||||||
|
import { CreateAlertRuleRequest } from '@/lib/api'
|
||||||
|
|
||||||
|
type RuleFormCardProps = {
|
||||||
|
rule: CreateAlertRuleRequest
|
||||||
|
index: number
|
||||||
|
totalRules: number
|
||||||
|
onUpdate: (index: number, updates: Partial<CreateAlertRuleRequest>) => void
|
||||||
|
onRemove: (index: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RuleFormCard({ rule, index, totalRules, onUpdate, onRemove }: RuleFormCardProps) {
|
||||||
|
const needsMax = COMPARISON_TYPES.find(c => c.value === rule.comparisonType)?.needsMax
|
||||||
|
const SensorIcon = getSensorIcon(rule.sensorType)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gradient-to-br from-orange-50 to-red-50 border border-orange-200 rounded-xl p-3 relative shadow-sm">
|
||||||
|
{/* Header - Compact */}
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs font-bold text-orange-700 bg-white/80 px-2 py-0.5 rounded-full shadow-sm">
|
||||||
|
{totalRules === 1 ? '⚡ شرط' : `⚡ شرط ${index + 1}`}
|
||||||
|
</span>
|
||||||
|
{/* Compact summary of current selection */}
|
||||||
|
<span className="text-xs text-gray-500 hidden sm:inline">
|
||||||
|
<SensorIcon className="w-3 h-3 inline ml-1" />
|
||||||
|
{getSensorLabel(rule.sensorType)}
|
||||||
|
{' | '}
|
||||||
|
{getComparisonLabel(rule.comparisonType)}
|
||||||
|
{' | '}
|
||||||
|
{rule.value1}{needsMax ? ` - ${rule.value2 ?? '?'}` : ''} {getSensorUnit(rule.sensorType)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{totalRules > 1 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onRemove(index)}
|
||||||
|
className="p-1.5 text-red-500 hover:bg-red-100 rounded-lg transition-colors"
|
||||||
|
title="حذف این شرط"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Compact row: Sensor | Comparison | Value */}
|
||||||
|
<div className="flex flex-col sm:flex-row gap-2">
|
||||||
|
{/* Sensor Type - Compact Select */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<select
|
||||||
|
value={rule.sensorType}
|
||||||
|
onChange={(e) => onUpdate(index, { sensorType: Number(e.target.value) as SensorType })}
|
||||||
|
className="w-full appearance-none px-3 py-2 pr-8 bg-white border border-orange-200 rounded-lg text-sm font-medium text-gray-700 focus:ring-2 focus:ring-orange-500 focus:border-orange-500 cursor-pointer"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")`,
|
||||||
|
backgroundPosition: 'left 0.5rem center',
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
backgroundSize: '1.25rem'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{SENSOR_TYPES.map(sensor => (
|
||||||
|
<option key={sensor.value} value={sensor.value}>
|
||||||
|
{sensor.label} ({sensor.unit})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Comparison Type - Compact Select */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<select
|
||||||
|
value={rule.comparisonType}
|
||||||
|
onChange={(e) => onUpdate(index, { comparisonType: Number(e.target.value) as ComparisonType })}
|
||||||
|
className="w-full appearance-none px-3 py-2 pr-8 bg-white border border-orange-200 rounded-lg text-sm font-medium text-gray-700 focus:ring-2 focus:ring-orange-500 focus:border-orange-500 cursor-pointer"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")`,
|
||||||
|
backgroundPosition: 'left 0.5rem center',
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
backgroundSize: '1.25rem'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{COMPARISON_TYPES.map(comp => (
|
||||||
|
<option key={comp.value} value={comp.value}>
|
||||||
|
{comp.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Threshold Values - Compact Inputs */}
|
||||||
|
<div className="flex gap-1.5 items-center flex-1">
|
||||||
|
<div className="relative flex-1 min-w-0">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={rule.value1}
|
||||||
|
onChange={(e) => onUpdate(index, { value1: Number(e.target.value) })}
|
||||||
|
className="w-full px-3 py-2 pl-10 bg-white border border-orange-200 rounded-lg text-sm font-medium focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
|
||||||
|
placeholder={needsMax ? 'از' : 'مقدار'}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<span className="absolute left-2 top-1/2 -translate-y-1/2 text-xs text-gray-400 font-medium">
|
||||||
|
{getSensorUnit(rule.sensorType)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{needsMax && (
|
||||||
|
<>
|
||||||
|
<span className="text-gray-400 text-sm flex-shrink-0">-</span>
|
||||||
|
<div className="relative flex-1 min-w-0">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={rule.value2 ?? ''}
|
||||||
|
onChange={(e) => onUpdate(index, { value2: Number(e.target.value) })}
|
||||||
|
className="w-full px-3 py-2 pl-10 bg-white border border-orange-200 rounded-lg text-sm font-medium focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
|
||||||
|
placeholder="تا"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<span className="absolute left-2 top-1/2 -translate-y-1/2 text-xs text-gray-400 font-medium">
|
||||||
|
{getSensorUnit(rule.sensorType)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
40
src/components/alert-settings/TimeTypeSelector.tsx
Normal file
40
src/components/alert-settings/TimeTypeSelector.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { TIME_TYPES, TimeType } from './constants'
|
||||||
|
|
||||||
|
type TimeTypeSelectorProps = {
|
||||||
|
value: TimeType
|
||||||
|
onChange: (value: TimeType) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TimeTypeSelector({ value, onChange }: TimeTypeSelectorProps) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-gray-500 mb-2">
|
||||||
|
زمان بررسی شرایط
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{TIME_TYPES.map(time => {
|
||||||
|
const Icon = time.icon
|
||||||
|
const isSelected = value === time.value
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={time.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onChange(time.value)}
|
||||||
|
title={time.description}
|
||||||
|
className={`inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg border transition-all text-xs ${
|
||||||
|
isSelected
|
||||||
|
? 'border-blue-500 bg-blue-50 text-blue-700 shadow-sm'
|
||||||
|
: 'border-gray-200 bg-white text-gray-600 hover:border-gray-300 hover:bg-gray-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon className={`w-3.5 h-3.5 ${isSelected ? 'text-blue-500' : 'text-gray-400'}`} />
|
||||||
|
<span className="font-medium">{time.label}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
59
src/components/alert-settings/constants.tsx
Normal file
59
src/components/alert-settings/constants.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import {
|
||||||
|
Thermometer,
|
||||||
|
Droplets,
|
||||||
|
Leaf,
|
||||||
|
Wind,
|
||||||
|
Sun,
|
||||||
|
ArrowLeftRight,
|
||||||
|
Maximize2,
|
||||||
|
Phone,
|
||||||
|
MessageSquare,
|
||||||
|
Plug,
|
||||||
|
Moon,
|
||||||
|
Zap,
|
||||||
|
LucideIcon
|
||||||
|
} from 'lucide-react'
|
||||||
|
|
||||||
|
export type SensorType = 0 | 1 | 2 | 3 | 4
|
||||||
|
export type ComparisonType = 0 | 1 | 2 | 3
|
||||||
|
export type NotificationType = number
|
||||||
|
export type TimeType = 0 | 1 | 2
|
||||||
|
|
||||||
|
export const NotifText: Record<number, string> = {
|
||||||
|
[-1]: 'بهت زنگ بزنم',
|
||||||
|
0: 'برات پیامک بفرستم',
|
||||||
|
1: 'رله یک رو فعال کنم',
|
||||||
|
2: 'رله دو رو فعال کنم',
|
||||||
|
3: 'رله سه رو فعال کنم',
|
||||||
|
4: 'رله چهار رو فعال کنم'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SENSOR_TYPES: { value: SensorType; label: string; icon: LucideIcon; unit: string }[] = [
|
||||||
|
{ value: 0, label: 'دما', icon: Thermometer, unit: '°C' },
|
||||||
|
{ value: 1, label: 'رطوبت هوا', icon: Droplets, unit: '%' },
|
||||||
|
{ value: 2, label: 'رطوبت خاک', icon: Leaf, unit: '%' },
|
||||||
|
{ value: 3, label: 'گاز', icon: Wind, unit: 'PPM' },
|
||||||
|
{ value: 4, label: 'نور', icon: Sun, unit: 'Lux' }
|
||||||
|
]
|
||||||
|
|
||||||
|
export const COMPARISON_TYPES: { value: ComparisonType; label: string; icon: LucideIcon | (() => React.JSX.Element); needsMax: boolean }[] = [
|
||||||
|
{ value: 0, label: 'بزرگتر از', icon: () => <span className="text-2xl font-bold">{'>'}</span>, needsMax: false },
|
||||||
|
{ value: 1, label: 'کوچکتر از', icon: () => <span className="text-2xl font-bold">{'<'}</span>, needsMax: false },
|
||||||
|
{ value: 2, label: 'بین', icon: ArrowLeftRight, needsMax: true },
|
||||||
|
{ value: 3, label: 'خارج از محدوده', icon: Maximize2, needsMax: true }
|
||||||
|
]
|
||||||
|
|
||||||
|
export const NOTIFICATION_TYPES: { value: NotificationType; label: string; icon: LucideIcon }[] = [
|
||||||
|
{ value: -1, label: 'تماس تلفنی', icon: Phone },
|
||||||
|
{ value: 0, label: 'پیامک', icon: MessageSquare },
|
||||||
|
{ value: 1, label: 'رله یک', icon: Plug },
|
||||||
|
{ value: 2, label: 'رله دو', icon: Plug },
|
||||||
|
{ value: 3, label: 'رله سه', icon: Plug },
|
||||||
|
{ value: 4, label: 'رله چهار', icon: Plug }
|
||||||
|
]
|
||||||
|
|
||||||
|
export const TIME_TYPES: { value: TimeType; label: string; icon: LucideIcon; description: string }[] = [
|
||||||
|
{ value: 0, label: 'روز', icon: Sun, description: 'فقط در روز بررسی شود' },
|
||||||
|
{ value: 1, label: 'شب', icon: Moon, description: 'فقط در شب بررسی شود' },
|
||||||
|
{ value: 2, label: 'همیشه', icon: Zap, description: 'در هر زمان بررسی شود' }
|
||||||
|
]
|
||||||
89
src/components/alert-settings/utils.ts
Normal file
89
src/components/alert-settings/utils.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { AlertRuleDto } from '@/lib/api'
|
||||||
|
import { SENSOR_TYPES, COMPARISON_TYPES, NOTIFICATION_TYPES, TIME_TYPES, NotifText, SensorType, ComparisonType, NotificationType, TimeType } from './constants'
|
||||||
|
import { AlertTriangle, LucideIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
export const getSensorIcon = (type: SensorType): LucideIcon => {
|
||||||
|
const sensor = SENSOR_TYPES.find(s => s.value === type)
|
||||||
|
return sensor ? sensor.icon : AlertTriangle
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSensorLabel = (type: SensorType) => {
|
||||||
|
const sensor = SENSOR_TYPES.find(s => s.value === type)
|
||||||
|
return sensor?.label ?? type.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSensorUnit = (type: SensorType) => {
|
||||||
|
const sensor = SENSOR_TYPES.find(s => s.value === type)
|
||||||
|
return sensor?.unit ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getComparisonLabel = (type: ComparisonType) => {
|
||||||
|
const comp = COMPARISON_TYPES.find(c => c.value === type)
|
||||||
|
return comp?.label ?? type.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getNotificationLabel = (type: NotificationType) => {
|
||||||
|
const notif = NOTIFICATION_TYPES.find(n => n.value === type)
|
||||||
|
return notif?.label ?? type.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTimeTypeLabel = (type: TimeType) => {
|
||||||
|
const time = TIME_TYPES.find(t => t.value === type)
|
||||||
|
return time?.label ?? type.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatRuleText = (rule: AlertRuleDto) => {
|
||||||
|
const comp = COMPARISON_TYPES.find(c => c.value === rule.comparisonType)
|
||||||
|
if (comp?.needsMax) {
|
||||||
|
return `${getComparisonLabel(rule.comparisonType)}: ${rule.value1} - ${rule.value2 ?? '?'} ${getSensorUnit(rule.sensorType)}`
|
||||||
|
}
|
||||||
|
const symbol = rule.comparisonType === 0 ? '>' : rule.comparisonType === 1 ? '<' : '?'
|
||||||
|
return `${symbol} ${rule.value1} ${getSensorUnit(rule.sensorType)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generatePreviewText = (
|
||||||
|
rules: { sensorType: SensorType; comparisonType: ComparisonType; value1: number; value2?: number | null }[],
|
||||||
|
notificationType: NotificationType,
|
||||||
|
timeType: TimeType
|
||||||
|
) => {
|
||||||
|
if (rules.length === 0) {
|
||||||
|
return '🤔 هنوز شرطی تعریف نکردی!'
|
||||||
|
}
|
||||||
|
|
||||||
|
const notifText = NotifText[notificationType] || ''
|
||||||
|
const timeText = timeType === 0
|
||||||
|
? 'فقط توی روز'
|
||||||
|
: timeType === 1
|
||||||
|
? 'فقط توی شب'
|
||||||
|
: ''
|
||||||
|
|
||||||
|
const rulesText = rules.map(rule => {
|
||||||
|
const sensorName = getSensorLabel(rule.sensorType)
|
||||||
|
const unit = getSensorUnit(rule.sensorType)
|
||||||
|
const comp = COMPARISON_TYPES.find(c => c.value === rule.comparisonType)
|
||||||
|
|
||||||
|
if (comp?.needsMax) {
|
||||||
|
if (rule.comparisonType === 2) {
|
||||||
|
return `${sensorName} بین ${rule.value1} تا ${rule.value2 ?? '؟'} ${unit} باشه`
|
||||||
|
} else {
|
||||||
|
return `${sensorName} از بازه ${rule.value1} تا ${rule.value2 ?? '؟'} ${unit} خارج شه`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const direction = rule.comparisonType === 0 ? 'بالاتر' : 'پایینتر'
|
||||||
|
return `${sensorName} ${direction} از ${rule.value1} ${unit} بره`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let baseText = ''
|
||||||
|
if (rulesText.length === 1) {
|
||||||
|
baseText = `وقتی ${rulesText[0]}`
|
||||||
|
} else {
|
||||||
|
const lastRule = rulesText[rulesText.length - 1]
|
||||||
|
const otherRules = rulesText.slice(0, -1).join(' و ')
|
||||||
|
baseText = `وقتی ${otherRules} و ${lastRule}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const timePrefix = timeText ? `${timeText}، ` : ''
|
||||||
|
|
||||||
|
return `${timePrefix}${baseText}، ${notifText} 📱`
|
||||||
|
}
|
||||||
@@ -56,7 +56,7 @@ export function Modal({
|
|||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{title && (
|
{title && (
|
||||||
<div className="flex items-center justify-between p-6 border-b border-gray-200 sticky top-0 bg-white z-10">
|
<div className="flex items-center justify-between px-6 border-b border-gray-200 sticky top-0 bg-white z-10">
|
||||||
<h2 className="text-xl font-bold text-gray-900">{title}</h2>
|
<h2 className="text-xl font-bold text-gray-900">{title}</h2>
|
||||||
{showCloseButton && (
|
{showCloseButton && (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export type AlertConditionDto = {
|
|||||||
|
|
||||||
export type CreateAlertConditionDto = {
|
export type CreateAlertConditionDto = {
|
||||||
deviceId: number
|
deviceId: number
|
||||||
notificationType: 0 | 1
|
notificationType: number
|
||||||
timeType: 0 | 1 | 2
|
timeType: 0 | 1 | 2
|
||||||
callCooldownMinutes?: number
|
callCooldownMinutes?: number
|
||||||
smsCooldownMinutes?: number
|
smsCooldownMinutes?: number
|
||||||
@@ -141,7 +141,7 @@ export type CreateAlertConditionDto = {
|
|||||||
|
|
||||||
export type UpdateAlertConditionDto = {
|
export type UpdateAlertConditionDto = {
|
||||||
id: number
|
id: number
|
||||||
notificationType: 0 | 1
|
notificationType: number
|
||||||
timeType: 0 | 1 | 2
|
timeType: 0 | 1 | 2
|
||||||
callCooldownMinutes?: number
|
callCooldownMinutes?: number
|
||||||
smsCooldownMinutes?: number
|
smsCooldownMinutes?: number
|
||||||
|
|||||||
Reference in New Issue
Block a user