Task 09 shipped a powerful API that a non-technical clinic owner could not safely use. This closes that gap: activation now requires having seen what the rule actually does. - PolicySimulator runs a policy against real past appointments and writes nothing: evaluation works on facts (never entities), the whole run sits in a transaction rolled back and cleared in `finally`, and a test counts rows in five sensitive tables before and after - activate() now demands a simulation of the *same version* — a report for version 1 does not unlock version 2 - PolicyTemplateRegistry: six ready-made rules, so the common case never touches a raw condition - Severity from the affected ratio; 0% is a warning too, since a rule that changes nothing usually has a condition that never matches - An empty clinic still succeeds with a warning, otherwise a new clinic could never activate anything Admin: PoliciesPage, PolicyFormPage, PolicySimulationPage, and a PolicyConditionBuilder built entirely from GET /policy-schema — a test proves a field that exists only in the schema shows up with no frontend change, and that operators are filtered per field type. The schema response now carries per-field metadata (label, type, meaningful operators) so the form has one source of truth instead of two. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
147 lines
4.7 KiB
TypeScript
147 lines
4.7 KiB
TypeScript
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
import { toast } from 'sonner';
|
|
import { api, ApiError, type ApiResponse } from '../lib/api';
|
|
import type {
|
|
Policy,
|
|
PolicyCategory,
|
|
PolicySchema,
|
|
PolicySimulationRun,
|
|
PolicyTemplate,
|
|
} from '../types';
|
|
|
|
/**
|
|
* قوانین کلینیک.
|
|
*
|
|
* دو نکتهٔ قراردادی که فرم به آنها وابسته است:
|
|
* ۱. schema و الگوها از سرور میآیند، نه از فهرستی در فرانت — یک حقیقت، نه دو تا.
|
|
* ۲. فعالسازی بدون آزمایشِ همان نسخه ۴۲۲ میگیرد؛ UI باید کاربر را اول به آزمایش ببرد.
|
|
*/
|
|
const POLICIES_KEY = ['policies'];
|
|
|
|
function fail(e: unknown, fallback: string) {
|
|
toast.error(e instanceof ApiError ? e.message : fallback);
|
|
}
|
|
|
|
/** schema تقریباً هرگز عوض نمیشود؛ نگهداشتن طولانیاش هزینهای ندارد. */
|
|
export function usePolicySchema() {
|
|
const query = useQuery({
|
|
queryKey: ['policy-schema'],
|
|
queryFn: () => api.get<ApiResponse<PolicySchema>>('/api/v1/policy-schema'),
|
|
staleTime: 300_000,
|
|
});
|
|
|
|
return { schema: query.data?.data, loading: query.isLoading };
|
|
}
|
|
|
|
export function usePolicyTemplates() {
|
|
const query = useQuery({
|
|
queryKey: ['policy-templates'],
|
|
queryFn: () => api.get<ApiResponse<PolicyTemplate[]>>('/api/v1/policy-templates'),
|
|
staleTime: 300_000,
|
|
});
|
|
|
|
return { templates: query.data?.data ?? [], loading: query.isLoading };
|
|
}
|
|
|
|
export function usePolicies(category?: PolicyCategory | '') {
|
|
const qc = useQueryClient();
|
|
const key = [...POLICIES_KEY, category ?? ''];
|
|
|
|
const query = useQuery({
|
|
queryKey: key,
|
|
queryFn: () =>
|
|
api.get<ApiResponse<Policy[]>>(
|
|
`/api/v1/policies${category ? `?category=${category}` : ''}`,
|
|
),
|
|
});
|
|
|
|
const invalidate = () => qc.invalidateQueries({ queryKey: POLICIES_KEY });
|
|
|
|
const activate = useMutation({
|
|
mutationFn: (uuid: string) => api.post<ApiResponse<Policy>>(`/api/v1/policy/${uuid}/activate`, {}),
|
|
onSuccess: () => {
|
|
toast.success('قانون فعال شد');
|
|
invalidate();
|
|
},
|
|
onError: (e) => fail(e, 'فعالسازی ناموفق بود'),
|
|
});
|
|
|
|
const deactivate = useMutation({
|
|
mutationFn: (uuid: string) => api.post<ApiResponse<Policy>>(`/api/v1/policy/${uuid}/deactivate`, {}),
|
|
onSuccess: () => {
|
|
toast.success('قانون غیرفعال شد');
|
|
invalidate();
|
|
},
|
|
onError: (e) => fail(e, 'غیرفعالسازی ناموفق بود'),
|
|
});
|
|
|
|
return {
|
|
policies: query.data?.data ?? [],
|
|
loading: query.isLoading,
|
|
activate,
|
|
deactivate,
|
|
};
|
|
}
|
|
|
|
export function usePolicy(uuid: string | undefined) {
|
|
const query = useQuery({
|
|
queryKey: ['policy', uuid],
|
|
queryFn: () => api.get<ApiResponse<Policy>>(`/api/v1/policy/${uuid}`),
|
|
enabled: !!uuid,
|
|
});
|
|
|
|
return { policy: query.data?.data, loading: query.isLoading };
|
|
}
|
|
|
|
export function usePolicyMutations() {
|
|
const qc = useQueryClient();
|
|
|
|
const create = useMutation({
|
|
mutationFn: (body: Record<string, unknown>) =>
|
|
api.post<ApiResponse<Policy>>('/api/v1/policy', body),
|
|
onSuccess: () => {
|
|
toast.success('قانون ساخته شد — حالا آزمایشش کنید');
|
|
qc.invalidateQueries({ queryKey: POLICIES_KEY });
|
|
},
|
|
onError: (e) => fail(e, 'ساخت قانون ناموفق بود'),
|
|
});
|
|
|
|
const newVersion = useMutation({
|
|
mutationFn: ({ uuid, body }: { uuid: string; body: Record<string, unknown> }) =>
|
|
api.post<ApiResponse<Policy>>(`/api/v1/policy/${uuid}/version`, body),
|
|
onSuccess: (_d, v) => {
|
|
toast.success('نسخهٔ تازه ثبت شد');
|
|
qc.invalidateQueries({ queryKey: POLICIES_KEY });
|
|
qc.invalidateQueries({ queryKey: ['policy', v.uuid] });
|
|
},
|
|
onError: (e) => fail(e, 'ثبت نسخه ناموفق بود'),
|
|
});
|
|
|
|
return { create, newVersion };
|
|
}
|
|
|
|
export function usePolicySimulation(uuid: string | undefined) {
|
|
const qc = useQueryClient();
|
|
const key = ['policy-simulations', uuid];
|
|
|
|
const history = useQuery({
|
|
queryKey: key,
|
|
queryFn: () => api.get<ApiResponse<PolicySimulationRun[]>>(`/api/v1/policy/${uuid}/simulations`),
|
|
enabled: !!uuid,
|
|
});
|
|
|
|
const run = useMutation({
|
|
mutationFn: (sampleSize?: number) =>
|
|
api.post<ApiResponse<PolicySimulationRun>>(`/api/v1/policy/${uuid}/simulate`, {
|
|
sample_size: sampleSize ?? 50,
|
|
}),
|
|
onSuccess: () => {
|
|
qc.invalidateQueries({ queryKey: key });
|
|
qc.invalidateQueries({ queryKey: POLICIES_KEY });
|
|
},
|
|
onError: (e) => fail(e, 'اجرای آزمایشی ناموفق بود'),
|
|
});
|
|
|
|
return { runs: history.data?.data ?? [], loading: history.isLoading, run };
|
|
}
|