diff --git a/src/app/alert-settings/page.tsx b/src/app/alert-settings/page.tsx index 688fa8d..8125ead 100644 --- a/src/app/alert-settings/page.tsx +++ b/src/app/alert-settings/page.tsx @@ -1,761 +1,153 @@ "use client" -import React, { useEffect, useState, useCallback, Suspense } from 'react' +import { useEffect, useState, useCallback, Suspense } from 'react' import { useSearchParams } from 'next/navigation' -import { - api, - 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 { api, AlertConditionDto, UpdateAlertConditionDto, CreateAlertConditionDto, CreateAlertRuleRequest } from '@/lib/api' +import { Bell, Plus, AlertTriangle } from 'lucide-react' 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' - -type SensorType = 0 | 1 | 2 | 3 | 4 -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: () => >, needsMax: false }, - { value: 1, label: 'کوچکتر از', icon: () => <, 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: 'در هر زمان بررسی شود' } -] +import { AlertListItem } from '@/components/alert-settings/AlertListItem' +import { AlertFormModal } from '@/components/alert-settings/AlertFormModal' function AlertSettingsContent() { const searchParams = useSearchParams() const deviceId = Number(searchParams.get('deviceId') ?? '1') - + const [alerts, setAlerts] = useState([]) const [loading, setLoading] = useState(true) - const [showModal, setShowModal] = useState(false) - const [editingAlert, setEditingAlert] = useState(null) + const [modal, setModal] = useState<{ open: boolean; editing: AlertConditionDto | null }>({ open: false, editing: null }) const [saving, setSaving] = useState(false) - - // Form state const [formData, setFormData] = useState({ - deviceId: deviceId, + deviceId, notificationType: 0, timeType: 2, isEnabled: true, - rules: [] + rules: [{ sensorType: 0, comparisonType: 0, value1: 30, order: 0 }] }) const loadAlerts = useCallback(async () => { setLoading(true) - try { - const data = await api.getAlertConditions(deviceId) - setAlerts(data) - } catch (error) { - console.error('Error loading alerts:', error) - } finally { - setLoading(false) - } + try { setAlerts(await api.getAlertConditions(deviceId)) } + catch (e) { console.error('Error loading alerts:', e) } + finally { setLoading(false) } }, [deviceId]) - useEffect(() => { - loadAlerts() - }, [loadAlerts]) + useEffect(() => { loadAlerts() }, [loadAlerts]) - const openCreateModal = () => { - setEditingAlert(null) + const openCreate = () => { + setModal({ open: true, editing: null }) setFormData({ - deviceId: deviceId, + deviceId, notificationType: 0, timeType: 2, isEnabled: true, - rules: [{ - sensorType: 0, - comparisonType: 0, - value1: 30, - order: 0 - }] + rules: [{ sensorType: 0, comparisonType: 0, value1: 30, order: 0 }] }) - setShowModal(true) } - const openEditModal = (alert: AlertConditionDto) => { - setEditingAlert(alert) + const openEdit = (alert: AlertConditionDto) => { + setModal({ open: true, editing: alert }) setFormData({ notificationType: alert.notificationType, timeType: alert.timeType, isEnabled: alert.isEnabled, deviceId: alert.deviceId, - rules: alert.rules.map((r, idx) => ({ - sensorType: r.sensorType, - comparisonType: r.comparisonType, - value1: r.value1, - value2: r.value2, - order: idx + rules: alert.rules.map((r, i) => ({ + sensorType: r.sensorType, comparisonType: r.comparisonType, + value1: r.value1, value2: r.value2, order: i })) }) - setShowModal(true) } - const closeModal = () => { - setShowModal(false) - setEditingAlert(null) - } + const closeModal = () => setModal({ open: false, editing: null }) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() - - if (formData.rules.length === 0) { - window.alert('لطفاً حداقل یک قانون اضافه کنید') - return - } - + if (!formData.rules.length) { window.alert('لطفاً حداقل یک قانون اضافه کنید'); return } setSaving(true) - try { - if (editingAlert) { - // Update - const updateDto: UpdateAlertConditionDto = { - id: editingAlert.id, - ...formData - } - await api.updateAlertCondition(updateDto) - } else { - // Create - await api.createAlertCondition(formData) - } - + if (modal.editing) await api.updateAlertCondition({ id: modal.editing.id, ...formData }) + else await api.createAlertCondition(formData) await loadAlerts() closeModal() } catch (error) { console.error('Error saving alert:', error) window.alert('خطا در ذخیره هشدار. لطفاً دوباره تلاش کنید.') - } finally { - setSaving(false) - } + } finally { setSaving(false) } } const handleDelete = async (id: number) => { - const confirmed = await confirmDialog({ - message: 'آیا از حذف این هشدار اطمینان دارید؟', - variant: 'danger', - confirmText: 'حذف', - cancelText: 'انصراف' - }) - - if (!confirmed) return - - try { - await api.deleteAlertCondition(id) - await loadAlerts() - } catch (error) { - console.error('Error deleting alert:', error) - window.alert('خطا در حذف هشدار. لطفاً دوباره تلاش کنید.') - } + if (!await confirmDialog({ message: 'آیا از حذف این هشدار اطمینان دارید؟', variant: 'danger', confirmText: 'حذف', cancelText: 'انصراف' })) return + try { await api.deleteAlertCondition(id); await loadAlerts() } + catch (e) { console.error('Error deleting alert:', e); window.alert('خطا در حذف هشدار.') } } const toggleActive = async (alert: AlertConditionDto) => { try { - const updateDto: UpdateAlertConditionDto = { - id: alert.id, - notificationType: alert.notificationType, - timeType: alert.timeType, + await api.updateAlertCondition({ + id: alert.id, notificationType: alert.notificationType, timeType: alert.timeType, isEnabled: !alert.isEnabled, - rules: alert.rules.map((r, idx) => ({ - sensorType: r.sensorType, - comparisonType: r.comparisonType, - value1: r.value1, - value2: r.value2, - order: idx - })) - } - await api.updateAlertCondition(updateDto) + rules: alert.rules.map((r, i) => ({ sensorType: r.sensorType, comparisonType: r.comparisonType, value1: r.value1, value2: r.value2, order: i })) + }) await loadAlerts() - } catch (error) { - console.error('Error toggling alert:', error) - window.alert('خطا در تغییر وضعیت هشدار.') - } + } catch (e) { console.error('Error toggling alert:', e); window.alert('خطا در تغییر وضعیت هشدار.') } } - const addRule = () => { - setFormData({ - ...formData, - rules: [ - ...formData.rules, - { - sensorType: 0, - comparisonType: 0, - value1: 30, - order: formData.rules.length - } - ] - }) - } + const addRule = () => setFormData(f => ({ ...f, rules: [...f.rules, { sensorType: 0, comparisonType: 0, value1: 30, order: f.rules.length }] })) + const removeRule = (index: number) => setFormData(f => ({ ...f, rules: f.rules.filter((_, i) => i !== index).map((r, i) => ({ ...r, order: i })) })) + const updateRule = (index: number, updates: Partial) => setFormData(f => { + const rules = [...f.rules]; rules[index] = { ...rules[index], ...updates }; return { ...f, rules } + }) - 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) => { - 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} بره` - } - }) - - // ساخت جمله نهایی - 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 - } + if (loading) return return (
- {/* Header */} - - افزودن هشدار + } /> - {/* Alerts List */}
-

- لیست هشدارها ({alerts.length}) -

+

لیست هشدارها ({alerts.length})

{alerts.length === 0 ? ( - - - افزودن اولین هشدار - - } + افزودن اولین هشدار} /> ) : (
- {alerts.map((alert) => ( -
-
-
- {/* Header */} -
- n.value === alert.notificationType)?.icon} - > - {getNotificationLabel(alert.notificationType)} - - t.value === alert.timeType)?.icon} - > - {getTimeTypeLabel(alert.timeType)} - - -
- - {/* Rules */} -
-
شرایط:
-
- {alert.rules.map((rule, idx) => { - const Icon = getSensorIcon(rule.sensorType) - return ( -
- - {getSensorLabel(rule.sensorType)} - {formatRuleText(rule)} -
- ) - })} -
-
-
- - {/* Actions */} -
- openEditModal(alert)} - title="ویرایش" - /> - handleDelete(alert.id)} - title="حذف" - /> -
-
-
+ {alerts.map(alert => ( + ))}
)}
- {/* Modal */} - - -
- {/* Preview - Sticky at top */} -
-
-
-
- -
-
-
📢 پیش‌نمایش زنده
-
- {generatePreviewText()} -
-
-
-
-
- -
- {/* Notification Type */} -
- -
- {NOTIFICATION_TYPES.map(notif => { - const Icon = notif.icon - const isSelected = formData.notificationType === notif.value - return ( - - ) - })} -
-
- - {/* Time Type */} -
- -

- مشخص کنید شرایط در چه زمانی بررسی و هشدار ارسال شود -

-
- {TIME_TYPES.map(time => { - const Icon = time.icon - const isSelected = formData.timeType === time.value - return ( - - ) - })} -
-
- - {/* Rules */} -
-
-
- -

- {formData.rules.length === 0 - ? 'هیچ شرطی تعریف نشده است' - : formData.rules.length === 1 - ? 'یک شرط تعریف شده است' - : `${formData.rules.length} شرط تعریف شده است (همه باید برقرار باشند)` - } -

-
- -
- -
- {formData.rules.map((rule, index) => { - const needsMax = COMPARISON_TYPES.find(c => c.value === rule.comparisonType)?.needsMax - - return ( -
-
-
- - {formData.rules.length === 1 ? '⚡ شرط' : `⚡ شرط ${index + 1}`} - -
- {formData.rules.length > 1 && ( - - )} -
- - {/* Sensor Type - Dropdown Style */} -
- -
- {SENSOR_TYPES.map(sensor => { - const Icon = sensor.icon - const isSelected = rule.sensorType === sensor.value - return ( - - ) - })} -
-
- - {/* Comparison Type - Card Style */} -
- -
- {COMPARISON_TYPES.map(comp => { - const Icon = comp.icon - const isSelected = rule.comparisonType === comp.value - return ( - - ) - })} -
-
- - {/* Threshold Values */} -
- -
-
-
- 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 - /> - - {getSensorUnit(rule.sensorType)} - -
-
- {needsMax && ( -
-
- 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 - /> - - {getSensorUnit(rule.sensorType)} - -
-
- )} -
-
-
- ) - })} -
-
- - {/* Active Status */} -
- setFormData({ ...formData, isEnabled: e.target.checked })} - className="w-4 h-4 text-orange-600 border-gray-300 rounded focus:ring-orange-500" - /> - -
- - {/* Modal Footer */} -
- - -
-
-
-
+ onSubmit={handleSubmit} + onFormDataChange={setFormData} + onAddRule={addRule} + onRemoveRule={removeRule} + onUpdateRule={updateRule} + />
) } diff --git a/src/components/alert-settings/AlertFormModal.tsx b/src/components/alert-settings/AlertFormModal.tsx new file mode 100644 index 0000000..b71a6ce --- /dev/null +++ b/src/components/alert-settings/AlertFormModal.tsx @@ -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) => void +} + +export function AlertFormModal({ + isOpen, + editingAlert, + saving, + formData, + onClose, + onSubmit, + onFormDataChange, + onAddRule, + onRemoveRule, + onUpdateRule +}: AlertFormModalProps) { + return ( + +
+ + +
+ onFormDataChange({ ...formData, notificationType: value })} + /> + + onFormDataChange({ ...formData, timeType: value })} + /> + + {/* Rules */} +
+
+
+ +

+ {formData.rules.length === 0 + ? 'هیچ شرطی تعریف نشده است' + : formData.rules.length === 1 + ? 'یک شرط تعریف شده است' + : `${formData.rules.length} شرط تعریف شده است` + } +

+
+ +
+ +
+ {formData.rules.map((rule, index) => ( + + ))} +
+
+ + {/* Active Status */} +
+ onFormDataChange({ ...formData, isEnabled: e.target.checked })} + className="w-3.5 h-3.5 text-orange-600 border-gray-300 rounded focus:ring-orange-500" + /> + +
+ + {/* Modal Footer */} +
+ + +
+
+ +
+ ) +} diff --git a/src/components/alert-settings/AlertListItem.tsx b/src/components/alert-settings/AlertListItem.tsx new file mode 100644 index 0000000..b8b796a --- /dev/null +++ b/src/components/alert-settings/AlertListItem.tsx @@ -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 ( +
+
+
+ {/* Header */} +
+ n.value === alert.notificationType)?.icon} + > + {getNotificationLabel(alert.notificationType)} + + t.value === alert.timeType)?.icon} + > + {getTimeTypeLabel(alert.timeType)} + + +
+ + {/* Rules */} +
+
شرایط:
+
+ {alert.rules.map((rule, idx) => { + const Icon = getSensorIcon(rule.sensorType) + return ( +
+ + {getSensorLabel(rule.sensorType)} + {formatRuleText(rule)} +
+ ) + })} +
+
+
+ + {/* Actions */} +
+ onEdit(alert)} title="ویرایش" /> + onDelete(alert.id)} title="حذف" /> +
+
+
+ ) +} diff --git a/src/components/alert-settings/AlertPreview.tsx b/src/components/alert-settings/AlertPreview.tsx new file mode 100644 index 0000000..7145602 --- /dev/null +++ b/src/components/alert-settings/AlertPreview.tsx @@ -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 ( +
+
+
+ {/*
+ +
*/} +
+ {/*
📢 پیش‌نمایش زنده
*/} +
+ {generatePreviewText(rules, notificationType, timeType)} +
+
+
+
+
+ ) +} diff --git a/src/components/alert-settings/NotificationTypeSelector.tsx b/src/components/alert-settings/NotificationTypeSelector.tsx new file mode 100644 index 0000000..52081b4 --- /dev/null +++ b/src/components/alert-settings/NotificationTypeSelector.tsx @@ -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 ( +
+ +
+ {NOTIFICATION_TYPES.map(notif => { + const Icon = notif.icon + const isSelected = value === notif.value + return ( + + ) + })} +
+
+ ) +} diff --git a/src/components/alert-settings/RuleFormCard.tsx b/src/components/alert-settings/RuleFormCard.tsx new file mode 100644 index 0000000..9f1f22e --- /dev/null +++ b/src/components/alert-settings/RuleFormCard.tsx @@ -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) => 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 ( +
+ {/* Header - Compact */} +
+
+ + {totalRules === 1 ? '⚡ شرط' : `⚡ شرط ${index + 1}`} + + {/* Compact summary of current selection */} + + + {getSensorLabel(rule.sensorType)} + {' | '} + {getComparisonLabel(rule.comparisonType)} + {' | '} + {rule.value1}{needsMax ? ` - ${rule.value2 ?? '?'}` : ''} {getSensorUnit(rule.sensorType)} + +
+ {totalRules > 1 && ( + + )} +
+ + {/* Compact row: Sensor | Comparison | Value */} +
+ {/* Sensor Type - Compact Select */} +
+ +
+ + {/* Comparison Type - Compact Select */} +
+ +
+ + {/* Threshold Values - Compact Inputs */} +
+
+ 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 + /> + + {getSensorUnit(rule.sensorType)} + +
+ {needsMax && ( + <> + - +
+ 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 + /> + + {getSensorUnit(rule.sensorType)} + +
+ + )} +
+
+
+ ) +} diff --git a/src/components/alert-settings/TimeTypeSelector.tsx b/src/components/alert-settings/TimeTypeSelector.tsx new file mode 100644 index 0000000..b0d3556 --- /dev/null +++ b/src/components/alert-settings/TimeTypeSelector.tsx @@ -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 ( +
+ +
+ {TIME_TYPES.map(time => { + const Icon = time.icon + const isSelected = value === time.value + return ( + + ) + })} +
+
+ ) +} diff --git a/src/components/alert-settings/constants.tsx b/src/components/alert-settings/constants.tsx new file mode 100644 index 0000000..46ba98b --- /dev/null +++ b/src/components/alert-settings/constants.tsx @@ -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 = { + [-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: () => {'>'}, needsMax: false }, + { value: 1, label: 'کوچکتر از', icon: () => {'<'}, 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: 'در هر زمان بررسی شود' } +] diff --git a/src/components/alert-settings/utils.ts b/src/components/alert-settings/utils.ts new file mode 100644 index 0000000..9321a46 --- /dev/null +++ b/src/components/alert-settings/utils.ts @@ -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} 📱` +} diff --git a/src/components/common/Modal.tsx b/src/components/common/Modal.tsx index d6616ec..11dc8b3 100644 --- a/src/components/common/Modal.tsx +++ b/src/components/common/Modal.tsx @@ -56,7 +56,7 @@ export function Modal({ onClick={(e) => e.stopPropagation()} > {title && ( -
+

{title}

{showCloseButton && (