feat(subscription): implement resource quota management based on subscription plans

This commit is contained in:
hamed
2026-08-04 19:38:49 +03:30
parent 0dca245246
commit 3e7028d77a
16 changed files with 408 additions and 18 deletions
+31
View File
@@ -1,6 +1,7 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { api, ApiError, type ApiResponse } from '../lib/api';
import { useSubscription } from './useSubscription';
import type {
ClinicResource, ResourcePool, ResourcePayload, ResourceServiceOffering, ResourceType, Skill,
} from '../types';
@@ -99,6 +100,36 @@ export function useResources(filters: ResourceFilters = {}) {
};
}
/**
* سهمیهٔ منابع محیط — سقفش از پلن اشتراک می‌آید و مصرفش از فهرست منابع.
*
* فهرست عمداً بی‌فیلتر خوانده می‌شود: سهمیه به کل محیط مربوط است و شمردنِ ردیف‌های
* فیلترشدهٔ صفحه، با هر فیلتر یک عدد متفاوت می‌داد. کوئری با کلیدِ `[resources, {}]`
* همان کوئریِ حالتِ بی‌فیلترِ صفحه است، پس معمولاً رفت‌وبرگشت اضافه‌ای نمی‌سازد.
*
* تصمیم نهایی با سرور است؛ این فقط دکمه را پیش از رفتن به فرم می‌بندد.
*/
export function useResourceQuota() {
const { maxResources, planLoaded } = useSubscription();
const { data } = useQuery({
queryKey: [RESOURCES_KEY, {}],
queryFn: () => api.get<ApiResponse<ClinicResource[]>>('/api/v1/resources'),
});
const used = data?.data?.length ?? 0;
// سقفِ ناشناخته (نقش بی‌اشتراک، یا پاسخِ هنوز نرسیده) گیت نمی‌شود؛ سرور خودش
// ۴۲۲ می‌دهد و بستنِ دکمه بر اساس حدس، بدتر از بستنش دیرتر است.
const unlimited = !planLoaded || maxResources < 0;
return {
used,
limit: maxResources,
unlimited,
atLimit: !unlimited && used >= maxResources,
};
}
/**
* سرویس‌هایی که یک منبع ارائه می‌دهد.
*
+7
View File
@@ -19,12 +19,19 @@ export function useSubscription() {
const effectivePlan = data?.data?.effective_plan ?? sub?.plan ?? null;
const features: Record<string, boolean> = effectivePlan?.features ?? {};
const maxSecretaries: number = effectivePlan?.max_secretaries ?? 1;
// `-1` یعنی بی‌نهایت.
const maxResources: number = effectivePlan?.max_resources ?? 1;
// نقش‌هایی که اشتراک ندارند (ادمین) و لحظهٔ پیش از رسیدن پاسخ: سقف ناشناخته است و
// نباید با پیش‌فرضِ ۱ به‌جای کاربر تصمیم گرفت — گیت‌کردن کارِ سرور است.
const planLoaded = effectivePlan !== null;
const hasPlan = sub !== null;
return {
subscription: sub,
hasFeature: (key: string) => features[key] ?? false,
maxSecretaries,
maxResources,
planLoaded,
hasPlan,
isExpiringSoon: (sub?.days_remaining ?? 0) > 0 && (sub?.days_remaining ?? 0) <= 7,
};