fix: align frontend bulk action types with backend API contract

- Fix BulkActionType enum: 'extend' → 'extend_subscription',
  'cancel' → 'cancel_subscription', 'activate' → 'activate_subscription'
- Fix BulkActionParams: 'balance_kopeks' → 'amount_kopeks' to match backend
- Fix SSE complete handler: build errors from progress log (backend
  complete event has no errors array)
- Add skipped_count to all BulkActionResult construction paths
- Add dry_run support to BulkActionRequest type
- Transform backend results[] to frontend errors[] in non-streaming path
- Remove unused errors variable in streaming fallback path
This commit is contained in:
Fringg
2026-04-24 04:52:30 +03:00
parent 312e0b4927
commit fc8170fa1f
2 changed files with 66 additions and 30 deletions

View File

@@ -2,9 +2,10 @@ import apiClient from './client';
import { tokenStorage } from '../utils/token';
export type BulkActionType =
| 'extend'
| 'cancel'
| 'activate'
| 'extend_subscription'
| 'add_days'
| 'cancel_subscription'
| 'activate_subscription'
| 'change_tariff'
| 'add_traffic'
| 'add_balance'
@@ -15,13 +16,15 @@ export interface BulkActionRequest {
action: BulkActionType;
user_ids: number[];
params: BulkActionParams;
dry_run?: boolean;
}
export interface BulkActionParams {
days?: number;
tariff_id?: number;
traffic_gb?: number;
balance_kopeks?: number;
amount_kopeks?: number;
balance_description?: string;
promo_group_id?: number | null;
}
@@ -36,6 +39,7 @@ export interface BulkActionResult {
total: number;
success_count: number;
error_count: number;
skipped_count: number;
errors: BulkActionErrorItem[];
}
@@ -52,11 +56,10 @@ export interface BulkProgressEvent {
export interface BulkCompleteEvent {
type: 'complete';
success: boolean;
total: number;
success_count: number;
error_count: number;
errors: BulkActionErrorItem[];
skipped_count: number;
}
export type BulkSSEEvent = BulkProgressEvent | BulkCompleteEvent;
@@ -66,7 +69,23 @@ const API_BASE_URL = import.meta.env.VITE_API_URL || '/api';
export const adminBulkActionsApi = {
execute: async (data: BulkActionRequest): Promise<BulkActionResult> => {
const response = await apiClient.post('/cabinet/admin/bulk/execute', data);
return response.data;
const raw = response.data;
// Transform backend results[] to frontend errors[]
const errors: BulkActionErrorItem[] = (raw.results || [])
.filter((r: { success: boolean }) => !r.success)
.map((r: { user_id: number; username?: string; message?: string }) => ({
user_id: r.user_id,
username: r.username,
error: r.message || 'Unknown error',
}));
return {
success: raw.error_count === 0,
total: raw.total,
success_count: raw.success_count,
error_count: raw.error_count,
skipped_count: raw.skipped_count || 0,
errors,
};
},
executeWithStream: async (
@@ -128,10 +147,13 @@ export const adminBulkActionsApi = {
}
} else {
// Fallback: non-streaming JSON response
const result = (await response.json()) as BulkActionResult;
const raw = await response.json();
onEvent({
type: 'complete',
...result,
total: raw.total,
success_count: raw.success_count,
error_count: raw.error_count,
skipped_count: raw.skipped_count || 0,
});
}
},

View File

@@ -462,9 +462,10 @@ function ActionModal({
if (!modal.open || !modal.action) return null;
const actionLabelKeys: Record<BulkActionType, string> = {
extend: 'admin.bulkActions.actions.extend',
cancel: 'admin.bulkActions.actions.cancel',
activate: 'admin.bulkActions.actions.activate',
extend_subscription: 'admin.bulkActions.actions.extend',
add_days: 'admin.bulkActions.actions.extend',
cancel_subscription: 'admin.bulkActions.actions.cancel',
activate_subscription: 'admin.bulkActions.actions.activate',
change_tariff: 'admin.bulkActions.actions.changeTariff',
add_traffic: 'admin.bulkActions.actions.addTraffic',
add_balance: 'admin.bulkActions.actions.addBalance',
@@ -475,7 +476,7 @@ function ActionModal({
const handleSubmit = () => {
const params: BulkActionParams = {};
switch (modal.action) {
case 'extend':
case 'extend_subscription':
params.days = days;
break;
case 'change_tariff':
@@ -485,7 +486,7 @@ function ActionModal({
params.traffic_gb = trafficGb;
break;
case 'add_balance':
params.balance_kopeks = Math.round(balanceRub * 100);
params.amount_kopeks = Math.round(balanceRub * 100);
break;
case 'assign_promo_group':
params.promo_group_id = promoGroupId;
@@ -506,7 +507,7 @@ function ActionModal({
const renderInputs = () => {
switch (modal.action) {
case 'extend':
case 'extend_subscription':
return (
<div>
<label className="mb-1.5 block text-sm font-medium text-dark-300">
@@ -781,19 +782,19 @@ function FloatingActionBar({
const actions: ActionConfig[] = [
{
type: 'extend',
type: 'extend_subscription',
labelKey: 'admin.bulkActions.actions.extend',
icon: <span aria-hidden="true">+</span>,
colorClass: 'text-success-400 hover:bg-success-500/10',
},
{
type: 'activate',
type: 'activate_subscription',
labelKey: 'admin.bulkActions.actions.activate',
icon: <span aria-hidden="true">+</span>,
colorClass: 'text-success-400 hover:bg-success-500/10',
},
{
type: 'cancel',
type: 'cancel_subscription',
labelKey: 'admin.bulkActions.actions.cancel',
icon: <span aria-hidden="true">-</span>,
colorClass: 'text-error-400 hover:bg-error-500/10',
@@ -1073,18 +1074,29 @@ export default function AdminBulkActions() {
: prev.progress,
}));
} else if (event.type === 'complete') {
setModal((prev) => ({
setModal((prev) => {
// Build errors from accumulated progress log
const errors = (prev.progress?.log ?? [])
.filter((e) => !e.success)
.map((e) => ({
user_id: e.user_id,
username: e.username,
error: e.message || e.error || '',
}));
return {
...prev,
loading: false,
progress: null,
result: {
success: event.success,
success: event.error_count === 0,
total: event.total,
success_count: event.success_count,
error_count: event.error_count,
errors: event.errors,
skipped_count: event.skipped_count || 0,
errors,
},
}));
};
});
loadUsers();
}
},
@@ -1101,6 +1113,7 @@ export default function AdminBulkActions() {
total: prev.progress.total,
success_count: prev.progress.successCount,
error_count: prev.progress.errorCount,
skipped_count: 0,
errors: prev.progress.log
.filter((e) => !e.success)
.map((e) => ({
@@ -1126,6 +1139,7 @@ export default function AdminBulkActions() {
total: totalCount,
success_count: prev.progress?.successCount ?? 0,
error_count: totalCount - (prev.progress?.successCount ?? 0),
skipped_count: 0,
errors: [],
},
}));