feat: Implement alert creation service with FCM notifications

- Add AlertCreationService to handle alert creation and notifications.
- Implement idempotency key support to prevent duplicate alerts.
- Create alert notification service for sending FCM messages.
- Add tests for alert creation and notification services.
- Document FCM notification flow and alert creation process.
This commit is contained in:
2026-07-11 01:18:18 +03:00
parent 93b266f26d
commit ddd67f438b
18 changed files with 1662 additions and 46 deletions

View File

@@ -66,12 +66,12 @@ export default function AlertsPage() {
}
};
const handleFormSubmit = async (data) => {
const handleFormSubmit = async (data, idempotencyKey) => {
try {
if (editingAlert) {
await alertsApi.update(editingAlert.id, data);
} else {
await alertsApi.create(data);
await alertsApi.create(data, idempotencyKey);
}
setShowForm(false);
loadData();
@@ -390,7 +390,17 @@ function AlertCard({ alert, index, onEdit, onDelete }) {
);
}
function createAlertIdempotencyKey() {
if (globalThis.crypto?.randomUUID) {
return globalThis.crypto.randomUUID();
}
return `alert-${Date.now()}-${Math.random().toString(36).slice(2)}`;
}
function AlertForm({ alert, routes, onSubmit, onCancel }) {
const [idempotencyKey] = useState(createAlertIdempotencyKey);
const [submitting, setSubmitting] = useState(false);
const [formData, setFormData] = useState({
route_id: alert?.route_id || '',
alert_type: alert?.alert_type || 'info',
@@ -401,7 +411,7 @@ function AlertForm({ alert, routes, onSubmit, onCancel }) {
is_active: alert?.is_active !== false
});
const handleSubmit = (e) => {
const handleSubmit = async (e) => {
e.preventDefault();
const data = {
@@ -410,7 +420,12 @@ function AlertForm({ alert, routes, onSubmit, onCancel }) {
end_time: formData.end_time || null
};
onSubmit(data);
setSubmitting(true);
try {
await onSubmit(data, alert ? null : idempotencyKey);
} finally {
setSubmitting(false);
}
};
const handleChange = (e) => {
@@ -546,15 +561,24 @@ function AlertForm({ alert, routes, onSubmit, onCancel }) {
</div>
<div className="p-4 sm:p-6 border-t border-gray-200 flex flex-col-reverse sm:flex-row sm:justify-end gap-2 sm:gap-3 safe-bottom">
<button type="button" onClick={onCancel} className="btn-secondary w-full sm:w-auto">
<button
type="button"
onClick={onCancel}
className="btn-secondary w-full sm:w-auto"
disabled={submitting}
>
Отмена
</button>
<button type="submit" className="btn-primary w-full sm:w-auto">
Сохранить
<button
type="submit"
className="btn-primary w-full sm:w-auto"
disabled={submitting}
>
{submitting ? 'Сохранение…' : 'Сохранить'}
</button>
</div>
</form>
</div>
</div>
);
}
}

View File

@@ -155,7 +155,9 @@ export const alertsApi = {
getAll: (params) => api.get('/alerts', { params }),
getActive: (routeId) => api.get('/alerts/active', { params: { route_id: routeId } }),
getById: (id) => api.get(`/alerts/${id}`),
create: (data) => api.post('/alerts', data),
create: (data, idempotencyKey) => api.post('/alerts', data, {
headers: idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : undefined
}),
update: (id, data) => api.put(`/alerts/${id}`, data),
delete: (id) => api.delete(`/alerts/${id}`),
cleanup: () => api.post('/alerts/cleanup')