Commit b756531c by luoqi

feat: 列表页加诊所多选筛选

- ListPlansQuerySchema 加 targetClinicIds(逗号串 preprocess 拆数组)
- plan.service list 按 targetClinicId IN [...] 过滤(仍受 staff clinic 隔离 OR 取交集)
- plans-api 数组序列化为逗号串上线
- 前端 ClinicFilter(Popover + 复选框),选项来自 token dictionary.clinics(用户可见诊所),>1 诊所才显示
- 选中存进 query.targetClinicIds,切换回第一页 + 清选择

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent ed0dba69
...@@ -84,7 +84,9 @@ export class PlanService { ...@@ -84,7 +84,9 @@ export class PlanService {
// 显式 filter(覆盖 view 推断) // 显式 filter(覆盖 view 推断)
if (query.status) where.status = query.status; if (query.status) where.status = query.status;
if (query.targetClinicId) where.targetClinicId = query.targetClinicId; // 诊所过滤:多选优先(IN),否则单选。staff 仍受下方 clinic 隔离 OR 约束(取交集)。
if (query.targetClinicIds?.length) where.targetClinicId = { in: query.targetClinicIds };
else if (query.targetClinicId) where.targetClinicId = query.targetClinicId;
if (query.assigneeUserId) where.assigneeUserId = query.assigneeUserId; if (query.assigneeUserId) where.assigneeUserId = query.assigneeUserId;
if (query.scenario) { if (query.scenario) {
where.reasons = { some: { scenario: query.scenario } }; where.reasons = { some: { scenario: query.scenario } };
......
...@@ -17,6 +17,8 @@ export const plansApi = { ...@@ -17,6 +17,8 @@ export const plansApi = {
scenario: q.scenario, scenario: q.scenario,
status: q.status, status: q.status,
targetClinicId: q.targetClinicId, targetClinicId: q.targetClinicId,
// 诊所多选:逗号串上线(后端 preprocess 拆数组);空则不带
targetClinicIds: q.targetClinicIds?.length ? q.targetClinicIds.join(',') : undefined,
assigneeUserId: q.assigneeUserId, assigneeUserId: q.assigneeUserId,
keyword: q.keyword, keyword: q.keyword,
sort: q.sort, sort: q.sort,
......
...@@ -4,6 +4,7 @@ import Link from 'next/link'; ...@@ -4,6 +4,7 @@ import Link from 'next/link';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { import {
ChevronDown,
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
Flame, Flame,
...@@ -18,6 +19,7 @@ import { Permission } from '@pac/types'; ...@@ -18,6 +19,7 @@ import { Permission } from '@pac/types';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { import {
Select, Select,
SelectContent, SelectContent,
...@@ -213,6 +215,17 @@ export function PlansListApp() { ...@@ -213,6 +215,17 @@ export function PlansListApp() {
setQuery({ status: s, page: 1 }); setQuery({ status: s, page: 1 });
clearSelected(); clearSelected();
}; };
// 诊所多选筛选 — 选项来自 token dictionary.clinics(用户可见诊所);状态存进 query.targetClinicIds
const clinicOptions = useMemo(
() =>
Object.entries(user?.dictionary?.clinics ?? {}).map(([id, name]) => ({ id, name: String(name) })),
[user?.dictionary?.clinics],
);
const selectedClinics = query.targetClinicIds ?? [];
const setClinics = (ids: string[]) => {
setQuery({ targetClinicIds: ids.length ? ids : undefined, page: 1 });
clearSelected();
};
return ( return (
<div className="flex h-full min-h-screen flex-col bg-slate-50"> <div className="flex h-full min-h-screen flex-col bg-slate-50">
...@@ -237,6 +250,9 @@ export function PlansListApp() { ...@@ -237,6 +250,9 @@ export function PlansListApp() {
setStatus={setStatus} setStatus={setStatus}
sort={sort} sort={sort}
setSort={setSort} setSort={setSort}
clinicOptions={clinicOptions}
selectedClinics={selectedClinics}
setClinics={setClinics}
density={density} density={density}
setDensity={setDensity} setDensity={setDensity}
allSelected={visible.length > 0 && visible.every((p) => selected.has(p.id))} allSelected={visible.length > 0 && visible.every((p) => selected.has(p.id))}
...@@ -471,10 +487,65 @@ function ViewTabs({ ...@@ -471,10 +487,65 @@ function ViewTabs({
// FilterBar — 搜索 / 状态 / 排序 / 密度 / 全选本页 // FilterBar — 搜索 / 状态 / 排序 / 密度 / 全选本页
// ───────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────
/// 诊所多选筛选(Popover + 复选框)。选项来自 token dictionary.clinics(用户可见诊所)。
function ClinicFilter({
options,
selected,
onChange,
}: {
options: Array<{ id: string; name: string }>;
selected: string[];
onChange: (ids: string[]) => void;
}) {
const sel = new Set(selected);
const toggle = (id: string) => {
const next = new Set(sel);
if (next.has(id)) next.delete(id);
else next.add(id);
onChange([...next]);
};
const label = selected.length === 0 ? '全部诊所' : `诊所 · ${selected.length}`;
return (
<Popover>
<PopoverTrigger asChild>
<button className="inline-flex h-8 items-center gap-1.5 rounded-md border border-slate-200 bg-white px-3 text-xs hover:border-slate-300">
<span className={cn(selected.length ? 'font-medium text-slate-900' : 'text-slate-600')}>{label}</span>
<ChevronDown className="h-3.5 w-3.5 text-slate-400" />
</button>
</PopoverTrigger>
<PopoverContent align="start" className="w-56 max-h-72 overflow-y-auto p-1.5">
<div className="mb-1 flex items-center justify-between border-b border-slate-100 px-1.5 py-1">
<span className="text-[10.5px] text-slate-400">按诊所筛选(可多选)</span>
{selected.length > 0 && (
<button onClick={() => onChange([])} className="text-[10.5px] text-teal-600 hover:underline">
清空
</button>
)}
</div>
{options.map((o) => (
<label
key={o.id}
className="flex cursor-pointer items-center gap-2 rounded px-1.5 py-1.5 hover:bg-slate-50"
>
<input
type="checkbox"
className="h-3.5 w-3.5 accent-teal-600"
checked={sel.has(o.id)}
onChange={() => toggle(o.id)}
/>
<span className="truncate text-[12px] text-slate-700">{o.name}</span>
</label>
))}
</PopoverContent>
</Popover>
);
}
function FilterBar({ function FilterBar({
view, view,
q, setQ, status, setStatus, q, setQ, status, setStatus,
sort, setSort, density, setDensity, sort, setSort, density, setDensity,
clinicOptions, selectedClinics, setClinics,
allSelected, someSelected, onTogglePage, allSelected, someSelected, onTogglePage,
}: { }: {
view: View; view: View;
...@@ -486,6 +557,9 @@ function FilterBar({ ...@@ -486,6 +557,9 @@ function FilterBar({
setSort: (s: SortKey) => void; setSort: (s: SortKey) => void;
density: Density; density: Density;
setDensity: (d: Density) => void; setDensity: (d: Density) => void;
clinicOptions: Array<{ id: string; name: string }>;
selectedClinics: string[];
setClinics: (ids: string[]) => void;
allSelected: boolean; allSelected: boolean;
someSelected: boolean; someSelected: boolean;
onTogglePage: () => void; onTogglePage: () => void;
...@@ -534,6 +608,9 @@ function FilterBar({ ...@@ -534,6 +608,9 @@ function FilterBar({
</SelectContent> </SelectContent>
</Select> </Select>
)} )}
{clinicOptions.length > 1 && (
<ClinicFilter options={clinicOptions} selected={selectedClinics} onChange={setClinics} />
)}
<Select value={sort} onValueChange={(v) => setSort(v as SortKey)}> <Select value={sort} onValueChange={(v) => setSort(v as SortKey)}>
<SelectTrigger className="h-8 w-44 text-xs"><SelectValue placeholder="排序" /></SelectTrigger> <SelectTrigger className="h-8 w-44 text-xs"><SelectValue placeholder="排序" /></SelectTrigger>
<SelectContent> <SelectContent>
......
...@@ -129,6 +129,11 @@ export const ListPlansQuerySchema = z.object({ ...@@ -129,6 +129,11 @@ export const ListPlansQuerySchema = z.object({
status: PlanStatusSchema.optional(), status: PlanStatusSchema.optional(),
/// 按 target_clinic_id 过滤召回池(null target 跟着 staff 自己 clinicIds 决定可见性) /// 按 target_clinic_id 过滤召回池(null target 跟着 staff 自己 clinicIds 决定可见性)
targetClinicId: z.string().optional(), targetClinicId: z.string().optional(),
/// 按诊所多选过滤(target_clinic_id IN [...])。前端逗号串上线,这里 preprocess 拆数组。
targetClinicIds: z.preprocess(
(v) => (typeof v === 'string' ? v.split(',').map((s) => s.trim()).filter(Boolean) : v),
z.array(z.string()).optional(),
),
assigneeUserId: z.string().optional(), assigneeUserId: z.string().optional(),
view: z.enum(['pool', 'mine', 'all']).optional() view: z.enum(['pool', 'mine', 'all']).optional()
.describe('"pool" = active+unassigned; "mine" = assigned to caller; "all" = no extra filter'), .describe('"pool" = active+unassigned; "mine" = assigned to caller; "all" = no extra filter'),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment