mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
feat: devices in subscription rows, set_devices action, table columns
- Remove "Days" column from main table (each subscription shows own) - Tariff column shows ALL subscription tariffs as comma-separated list - Subscription sub-rows show device count with phone icon - New bulk action "Изменить устройства" (set_devices) with input 1-50 - Update UserListItemSubscription type with device_limit field - i18n: setDevices + deviceLimit keys (ru + en)
This commit is contained in:
@@ -10,7 +10,8 @@ export type BulkActionType =
|
|||||||
| 'add_traffic'
|
| 'add_traffic'
|
||||||
| 'add_balance'
|
| 'add_balance'
|
||||||
| 'assign_promo_group'
|
| 'assign_promo_group'
|
||||||
| 'grant_subscription';
|
| 'grant_subscription'
|
||||||
|
| 'set_devices';
|
||||||
|
|
||||||
export interface BulkActionRequest {
|
export interface BulkActionRequest {
|
||||||
action: BulkActionType;
|
action: BulkActionType;
|
||||||
@@ -27,6 +28,7 @@ export interface BulkActionParams {
|
|||||||
amount_kopeks?: number;
|
amount_kopeks?: number;
|
||||||
balance_description?: string;
|
balance_description?: string;
|
||||||
promo_group_id?: number | null;
|
promo_group_id?: number | null;
|
||||||
|
device_limit?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BulkActionErrorItem {
|
export interface BulkActionErrorItem {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export interface UserListItemSubscription {
|
|||||||
days_remaining: number;
|
days_remaining: number;
|
||||||
traffic_used_gb: number;
|
traffic_used_gb: number;
|
||||||
traffic_limit_gb: number;
|
traffic_limit_gb: number;
|
||||||
|
device_limit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserListItem {
|
export interface UserListItem {
|
||||||
|
|||||||
@@ -2039,7 +2039,8 @@
|
|||||||
"addTraffic": "Add traffic",
|
"addTraffic": "Add traffic",
|
||||||
"addBalance": "Add balance",
|
"addBalance": "Add balance",
|
||||||
"assignPromoGroup": "Assign promo group",
|
"assignPromoGroup": "Assign promo group",
|
||||||
"grantSubscription": "Grant subscription"
|
"grantSubscription": "Grant subscription",
|
||||||
|
"setDevices": "Set devices"
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"days": "Number of days",
|
"days": "Number of days",
|
||||||
@@ -2047,7 +2048,8 @@
|
|||||||
"trafficGb": "Traffic amount (GB)",
|
"trafficGb": "Traffic amount (GB)",
|
||||||
"balanceRub": "Amount (RUB)",
|
"balanceRub": "Amount (RUB)",
|
||||||
"promoGroup": "Promo group",
|
"promoGroup": "Promo group",
|
||||||
"removePromoGroup": "Remove promo group"
|
"removePromoGroup": "Remove promo group",
|
||||||
|
"deviceLimit": "Device count"
|
||||||
},
|
},
|
||||||
"grantSubscription": {
|
"grantSubscription": {
|
||||||
"warning": "Users who already have a subscription on the selected tariff will be skipped"
|
"warning": "Users who already have a subscription on the selected tariff will be skipped"
|
||||||
|
|||||||
@@ -3787,7 +3787,8 @@
|
|||||||
"addTraffic": "Начислить трафик",
|
"addTraffic": "Начислить трафик",
|
||||||
"addBalance": "Начислить баланс",
|
"addBalance": "Начислить баланс",
|
||||||
"assignPromoGroup": "Назначить промогруппу",
|
"assignPromoGroup": "Назначить промогруппу",
|
||||||
"grantSubscription": "Выдать подписку"
|
"grantSubscription": "Выдать подписку",
|
||||||
|
"setDevices": "Изменить устройства"
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"days": "Количество дней",
|
"days": "Количество дней",
|
||||||
@@ -3795,7 +3796,8 @@
|
|||||||
"trafficGb": "Объём трафика (ГБ)",
|
"trafficGb": "Объём трафика (ГБ)",
|
||||||
"balanceRub": "Сумма (руб.)",
|
"balanceRub": "Сумма (руб.)",
|
||||||
"promoGroup": "Промогруппа",
|
"promoGroup": "Промогруппа",
|
||||||
"removePromoGroup": "Убрать промогруппу"
|
"removePromoGroup": "Убрать промогруппу",
|
||||||
|
"deviceLimit": "Кол-во устройств"
|
||||||
},
|
},
|
||||||
"grantSubscription": {
|
"grantSubscription": {
|
||||||
"warning": "Пользователи, у которых уже есть подписка на выбранный тариф, будут пропущены"
|
"warning": "Пользователи, у которых уже есть подписка на выбранный тариф, будут пропущены"
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ const SUBSCRIPTION_LEVEL_ACTIONS: Set<BulkActionType> = new Set([
|
|||||||
'activate_subscription',
|
'activate_subscription',
|
||||||
'change_tariff',
|
'change_tariff',
|
||||||
'add_traffic',
|
'add_traffic',
|
||||||
|
'set_devices',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function isSubscriptionLevelAction(action: BulkActionType): boolean {
|
function isSubscriptionLevelAction(action: BulkActionType): boolean {
|
||||||
@@ -264,6 +265,24 @@ function SubscriptionSubRow({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Devices */}
|
||||||
|
<span className="flex items-center gap-1 text-xs text-dark-400">
|
||||||
|
<svg
|
||||||
|
className="h-3 w-3"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{subscription.device_limit}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -738,6 +757,7 @@ function ActionModal({
|
|||||||
const [promoGroupId, setPromoGroupId] = useState<number | null>(promoGroups[0]?.id ?? null);
|
const [promoGroupId, setPromoGroupId] = useState<number | null>(promoGroups[0]?.id ?? null);
|
||||||
const [grantTariffId, setGrantTariffId] = useState<number>(tariffs[0]?.id ?? 0);
|
const [grantTariffId, setGrantTariffId] = useState<number>(tariffs[0]?.id ?? 0);
|
||||||
const [grantDays, setGrantDays] = useState(30);
|
const [grantDays, setGrantDays] = useState(30);
|
||||||
|
const [deviceLimit, setDeviceLimit] = useState(1);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tariffs.length > 0 && tariffId === 0) {
|
if (tariffs.length > 0 && tariffId === 0) {
|
||||||
@@ -778,6 +798,7 @@ function ActionModal({
|
|||||||
add_balance: 'admin.bulkActions.actions.addBalance',
|
add_balance: 'admin.bulkActions.actions.addBalance',
|
||||||
assign_promo_group: 'admin.bulkActions.actions.assignPromoGroup',
|
assign_promo_group: 'admin.bulkActions.actions.assignPromoGroup',
|
||||||
grant_subscription: 'admin.bulkActions.actions.grantSubscription',
|
grant_subscription: 'admin.bulkActions.actions.grantSubscription',
|
||||||
|
set_devices: 'admin.bulkActions.actions.setDevices',
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
@@ -802,6 +823,9 @@ function ActionModal({
|
|||||||
params.tariff_id = grantTariffId;
|
params.tariff_id = grantTariffId;
|
||||||
params.days = grantDays;
|
params.days = grantDays;
|
||||||
break;
|
break;
|
||||||
|
case 'set_devices':
|
||||||
|
params.device_limit = deviceLimit;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
onExecute(params);
|
onExecute(params);
|
||||||
};
|
};
|
||||||
@@ -891,6 +915,22 @@ function ActionModal({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
case 'set_devices':
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label className="mb-1.5 block text-sm font-medium text-dark-300">
|
||||||
|
{t('admin.bulkActions.params.deviceLimit')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={50}
|
||||||
|
value={deviceLimit}
|
||||||
|
onChange={(e) => setDeviceLimit(Number(e.target.value))}
|
||||||
|
className="w-full rounded-xl border border-dark-700 bg-dark-800 px-3 py-2.5 text-sm text-dark-100 outline-none transition-colors focus:border-accent-500/40"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
case 'grant_subscription':
|
case 'grant_subscription':
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -1129,6 +1169,26 @@ function FloatingActionBar({
|
|||||||
icon: <span aria-hidden="true">+</span>,
|
icon: <span aria-hidden="true">+</span>,
|
||||||
colorClass: 'text-accent-400 hover:bg-accent-500/10',
|
colorClass: 'text-accent-400 hover:bg-accent-500/10',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'set_devices',
|
||||||
|
labelKey: 'admin.bulkActions.actions.setDevices',
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
className="h-3.5 w-3.5"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
colorClass: 'text-accent-400 hover:bg-accent-500/10',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'add_balance',
|
type: 'add_balance',
|
||||||
labelKey: 'admin.bulkActions.actions.addBalance',
|
labelKey: 'admin.bulkActions.actions.addBalance',
|
||||||
@@ -1780,13 +1840,34 @@ export default function AdminBulkActions() {
|
|||||||
id: 'tariff',
|
id: 'tariff',
|
||||||
accessorFn: (row) => row.tariff_name,
|
accessorFn: (row) => row.tariff_name,
|
||||||
header: t('admin.bulkActions.columns.tariff'),
|
header: t('admin.bulkActions.columns.tariff'),
|
||||||
size: 120,
|
size: 160,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const user = row.original;
|
const user = row.original;
|
||||||
if (!user.tariff_name) {
|
const subs = user.subscriptions ?? [];
|
||||||
return <span className="text-xs text-dark-500">—</span>;
|
const tariffNames = subs
|
||||||
|
.map((s) => s.tariff_name)
|
||||||
|
.filter((name): name is string => !!name);
|
||||||
|
const uniqueNames = [...new Set(tariffNames)];
|
||||||
|
|
||||||
|
if (uniqueNames.length === 0) {
|
||||||
|
if (!user.tariff_name) {
|
||||||
|
return <span className="text-xs text-dark-500">—</span>;
|
||||||
|
}
|
||||||
|
return <span className="text-xs text-dark-200">{user.tariff_name}</span>;
|
||||||
}
|
}
|
||||||
return <span className="text-xs text-dark-200">{user.tariff_name}</span>;
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{uniqueNames.map((name) => (
|
||||||
|
<span
|
||||||
|
key={name}
|
||||||
|
className="inline-flex rounded-md border border-dark-600/40 bg-dark-700/40 px-1.5 py-0.5 text-[10px] font-medium text-dark-200"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1800,33 +1881,6 @@ export default function AdminBulkActions() {
|
|||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'days_remaining',
|
|
||||||
header: t('admin.bulkActions.columns.daysRemaining'),
|
|
||||||
size: 80,
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const user = row.original;
|
|
||||||
if (!user.subscription_end_date) {
|
|
||||||
return <span className="text-xs text-dark-500">-</span>;
|
|
||||||
}
|
|
||||||
const end = new Date(user.subscription_end_date);
|
|
||||||
const now = new Date();
|
|
||||||
const days = Math.max(
|
|
||||||
0,
|
|
||||||
Math.ceil((end.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)),
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'text-xs font-medium tabular-nums',
|
|
||||||
days === 0 ? 'text-error-400' : days <= 3 ? 'text-warning-400' : 'text-dark-300',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{days}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'promo_group',
|
id: 'promo_group',
|
||||||
accessorFn: (row) => row.promo_group_name,
|
accessorFn: (row) => row.promo_group_name,
|
||||||
|
|||||||
Reference in New Issue
Block a user