refactor(services): drop the groups and segments tabs

Anything that belongs to a service is defined on the service itself, so the
two tabs that managed selection groups and appointment segments come off the
service page.

Only the UI goes. SegmentTemplate is what makes a service occupy a room and a
device at the same time — it is the input to AppointmentPlanBuilder and the
reason the resource timeline has anything to draw — and a service without a
template already books through singleSegment(). Removing the model would
change booking; removing the tabs does not, which tests/Appointment confirms
at 314 green.

The active tab moved into the query string on the way past. That is what
makes the old ?tab=segments link land on the info tab instead of rendering
nothing, and it lets back and refresh return to the same tab.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-02 15:56:49 +03:30
co-authored by Claude Opus 5
parent 9ba4c8d948
commit b8e8580867
7 changed files with 39 additions and 1046 deletions
+29 -2
View File
@@ -75,12 +75,12 @@ beforeEach(() => {
});
// صفحه uuid را از useParams می‌گیرد، پس به یک Route واقعی نیاز دارد.
const render = () =>
const render = (route = '/admin/clinic-services/it1') =>
renderWithProviders(
<Routes>
<Route path="/admin/clinic-services/:uuid" element={<ServiceDetailPage />} />
</Routes>,
{ route: '/admin/clinic-services/it1' },
{ route },
);
describe('ServiceDetailPage (جزئیات سرویس)', () => {
@@ -202,4 +202,31 @@ describe('ServiceDetailPage (جزئیات سرویس)', () => {
expect(await screen.findByText('غیرفعال')).toBeInTheDocument();
expect(screen.getByText('فعال‌کردن')).toBeInTheDocument();
});
/**
* گروه‌های انتخاب و بخش‌های نوبت از صفحهٔ سرویس برداشته شدند: هر چیزی که به یک سرویس
* مربوط است در خودِ سرویس تعریف می‌شود. مدل و موتور سر جایشان‌اند — تست‌های
* `tests/Appointment` همان رفتار قبلی را قفل می‌کنند.
*/
it('دیگر تب گروه‌ها و بخش‌های نوبت ندارد', async () => {
render();
await screen.findByRole('heading', { name: 'سرم ۵۰۰cc' });
expect(screen.queryByText('گروه‌ها و آیتم‌ها')).not.toBeInTheDocument();
expect(screen.queryByText('بخش‌های نوبت')).not.toBeInTheDocument();
});
/** لینک قدیمی به تبِ حذف‌شده نباید صفحهٔ خالی بدهد. */
it('تبِ ناشناخته در URL به اطلاعات برمی‌گردد', async () => {
render('/admin/clinic-services/it1?tab=segments');
expect(await screen.findByText('تاریخ ایجاد')).toBeInTheDocument();
});
/** تب در URL می‌نشیند تا «بازگشت» و رفرش همان نما را بدهند. */
it('تب انتخاب‌شده از URL خوانده می‌شود', async () => {
render('/admin/clinic-services/it1?tab=tariffs');
expect(await screen.findByText('سال جاری')).toBeInTheDocument();
});
});
+5 -7
View File
@@ -15,11 +15,10 @@ import PageHeader from '../components/ui/PageHeader';
import FeatureGate from '../components/ui/FeatureGate';
import ConfirmDialog from '../components/ui/ConfirmDialog';
import { usePermissions } from '../hooks/usePermissions';
import { useUrlState } from '../hooks/useUrlState';
import ServiceTariffModal from '../components/ServiceTariffModal';
import ServiceInsuranceModal from '../components/ServiceInsuranceModal';
import ServiceItemFormModal from '../components/ServiceItemFormModal';
import ServiceGroupsTab from '../components/ServiceGroupsTab';
import ServiceSegmentsTab from '../components/ServiceSegmentsTab';
import ServiceCategoryTab from '../components/ServiceCategoryTab';
interface Tariff {
@@ -54,8 +53,6 @@ const TABS = [
{ id: 'info', label: 'اطلاعات سرویس' },
{ id: 'tariffs', label: 'تعرفه‌ها' },
{ id: 'insurance', label: 'بیمه‌ها' },
{ id: 'groups', label: 'گروه‌ها و آیتم‌ها' },
{ id: 'segments', label: 'بخش‌های نوبت' },
{ id: 'categories',label: 'دسته‌بندی‌ها' },
{ id: 'goods', label: 'کالاهای مرتبط' },
{ id: 'history', label: 'لاگ تغییرات' },
@@ -458,7 +455,10 @@ function ServiceDetailPageInner() {
const { can } = usePermissions();
const canUpdate = can('services', 'update');
const [tab, setTab] = useState<TabId>('info');
const [urlState, setUrlState] = useUrlState({ tab: 'info' });
// تبِ ناشناخته (لینک قدیمی به گروه‌ها/بخش‌ها) به اطلاعات برمی‌گردد، نه صفحهٔ خالی.
const tab = (TABS.some((t) => t.id === urlState.tab) ? urlState.tab : 'info') as TabId;
const setTab = (id: TabId) => setUrlState({ tab: id });
const [editOpen, setEditOpen] = useState(false);
const [tariffOpen, setTariffOpen] = useState(false);
const [insuranceOpen, setInsuranceOpen] = useState(false);
@@ -543,8 +543,6 @@ function ServiceDetailPageInner() {
{tab === 'info' && <InfoTab item={item} />}
{tab === 'tariffs' && <TariffsTab item={item} onManage={() => setTariffOpen(true)} canUpdate={canUpdate} />}
{tab === 'insurance' && <InsuranceTab item={item} onManage={() => setInsuranceOpen(true)} canUpdate={canUpdate} />}
{tab === 'groups' && <ServiceGroupsTab serviceUuid={item.uuid} canEdit={canUpdate} />}
{tab === 'segments' && <ServiceSegmentsTab serviceUuid={item.uuid} canEdit={canUpdate} />}
{tab === 'categories' && (
<ServiceCategoryTab
serviceUuid={item.uuid}