'use client'; import { useState } from 'react'; import { useLocale, useTranslations } from 'next-intl'; import { useRouter } from 'next/navigation'; import { useSnackbar } from 'notistack'; import { Box, Dialog, DialogActions, DialogContent, DialogTitle, FormControlLabel, Skeleton, Stack, Switch, TextField, Typography, } from '@mui/material'; import { AppButton, StatusChip } from '@/components'; import type { StatusKind } from '@/components'; import { AdminDataTable, AdminEmptyState, AdminErrorState, AdminPageHeader, AdminPager, type AdminTableColumn, } from '@/components/admin'; import { useAdminCapabilities } from '@/hooks'; import { adminPartnerCenterPath } from '@/constants'; import { PARTNER_PAGE_SIZE } from '@/services/partnerCenter/constants'; import type { CenterOnboardingState, PartnerCenter, PartnerCenterInput } from '@/services/partnerCenter/types'; import { usePartnerCenters, useCreatePartnerCenter, useUpdatePartnerCenter } from '@/services/partnerCenter'; /** State → semantic chip color. verified = green, pending = amber, suspended = red, draft = neutral. */ export const CENTER_STATE_KIND: Record = { verified: 'verified', pending_verification: 'pending', suspended: 'rejected', draft: 'neutral', }; /** * Partner-center admin list (f15) — the licensed sponsoring centers (پروانه تأسیس + مسئول فنی + نماد * اعتماد الکترونیکی) that may be the merchant-of-record. Each row shows whether it issues invoices, its * sponsored-nurse count, and its onboarding state; a row opens the center detail. Admins with * `canManagePartners` may create a new center (inactive until verified). The full IBAN is write-then-masked — * it is only ever entered here, never displayed (the list carries no IBAN at all). */ export default function AdminPartnersPage() { const t = useTranslations('admin'); const locale = useLocale(); const router = useRouter(); const caps = useAdminCapabilities(); const [page, setPage] = useState(1); const [creating, setCreating] = useState(false); const centers = usePartnerCenters({}, page); const items = centers.data?.items ?? []; const pageCount = Math.max(1, Math.ceil((centers.data?.total ?? 0) / PARTNER_PAGE_SIZE)); const columns: AdminTableColumn[] = [ { key: 'name', header: t('partner_col_name'), render: (c) => c.name }, { key: 'mor', header: t('partner_col_mor'), render: (c) => t(c.isMerchantOfRecord ? 'yes' : 'no') }, { key: 'nurses', header: t('partner_col_nurses'), render: (c) => c.sponsoredNurseCount }, { key: 'state', header: t('partner_col_state'), render: (c) => , }, ]; return ( setCreating(true)} sx={{ m: 0 }}> {t('partner_create')} ) : undefined } /> {centers.isLoading ? ( ) : centers.isError ? ( centers.refetch()} /> ) : items.length === 0 ? ( ) : ( c.id} ariaLabel={t('partner_title')} onRowClick={(c) => router.push(`/${locale}${adminPartnerCenterPath(c.id)}`)} /> )} setPage((p) => Math.max(1, p - 1))} onNext={() => setPage((p) => Math.min(pageCount, p + 1))} prevLabel={t('prev_page')} nextLabel={t('next_page')} indicator={t('page_indicator', { page })} /> {creating ? setCreating(false)} /> : null} ); } /** The editable slice of `PartnerCenterInput`, kept as strings for controlled text/number inputs. */ interface CenterFormState { name: string; legalEntityType: string; mohEstablishmentPermitNo: string; technicalDirectorLicenseNo: string; enamadCode: string; settlementIban: string; isMerchantOfRecord: boolean; commissionRate: string; adminUserId: string; } function initialForm(center: PartnerCenter | null): CenterFormState { return { name: center?.name ?? '', legalEntityType: center?.legalEntityType ?? '', mohEstablishmentPermitNo: center?.mohEstablishmentPermitNo ?? '', technicalDirectorLicenseNo: center?.technicalDirectorLicenseNo ?? '', enamadCode: center?.enamadCode ?? '', // Write-then-masked: always blank on open. On edit, a blank IBAN keeps the existing masked value. settlementIban: '', isMerchantOfRecord: center?.isMerchantOfRecord ?? false, commissionRate: center != null ? String(center.commissionRate) : '', adminUserId: center?.adminUserId != null ? String(center.adminUserId) : '', }; } /** * The create/edit dialog for a partner center — shared by the list (create, `center=null`) and the detail * (edit, `center` prefilled). `settlementIban` is write-then-masked: the field is always blank on open and a * blank submit on edit keeps the stored masked value. Validates name + permit non-empty, `commissionRate ∈ * [0, 1)`, and (create only) an IBAN when the center is merchant-of-record. */ export function PartnerCenterFormDialog({ center, onClose }: { center: PartnerCenter | null; onClose: () => void }) { const t = useTranslations('admin'); const { enqueueSnackbar } = useSnackbar(); const isEdit = center != null; const create = useCreatePartnerCenter(); const update = useUpdatePartnerCenter(center?.id ?? 0); const mutation = isEdit ? update : create; const [form, setForm] = useState(() => initialForm(center)); const set = (key: K, value: CenterFormState[K]) => setForm((f) => ({ ...f, [key]: value })); const commission = Number(form.commissionRate); const commissionValid = form.commissionRate.trim() !== '' && Number.isFinite(commission) && commission >= 0 && commission < 1; // On edit a blank IBAN is allowed (it keeps the stored value); on create an MoR center must supply one. const ibanValid = !form.isMerchantOfRecord || isEdit || form.settlementIban.trim() !== ''; const valid = form.name.trim() !== '' && form.mohEstablishmentPermitNo.trim() !== '' && commissionValid && ibanValid; const onSave = () => { if (!valid) return; const input: PartnerCenterInput = { name: form.name.trim(), legalEntityType: form.legalEntityType.trim(), mohEstablishmentPermitNo: form.mohEstablishmentPermitNo.trim(), technicalDirectorLicenseNo: form.technicalDirectorLicenseNo.trim() || null, enamadCode: form.enamadCode.trim() || null, settlementIban: form.settlementIban.trim() || null, isMerchantOfRecord: form.isMerchantOfRecord, commissionRate: commission, adminUserId: form.adminUserId.trim() === '' ? null : Number(form.adminUserId), }; mutation.mutate(input, { onSuccess: () => { enqueueSnackbar(t('partner_saved'), { variant: 'success' }); onClose(); }, }); }; return ( {isEdit ? t('partner_edit') : t('partner_create')} set('name', e.target.value)} /> set('legalEntityType', e.target.value)} /> set('mohEstablishmentPermitNo', e.target.value)} /> set('technicalDirectorLicenseNo', e.target.value)} /> set('enamadCode', e.target.value)} /> set('settlementIban', e.target.value)} helperText={t('partner_iban_write_hint')} placeholder={center?.settlementIbanMasked ?? undefined} slotProps={{ htmlInput: { dir: 'ltr' } }} /> set('isMerchantOfRecord', e.target.checked)} />} label={t('partner_is_mor')} /> {t('partner_is_mor_hint')} set('commissionRate', e.target.value)} slotProps={{ htmlInput: { min: 0, max: 0.999, step: 0.01 } }} /> set('adminUserId', e.target.value)} slotProps={{ htmlInput: { min: 1, step: 1 } }} /> {t('cancel')} {mutation.isPending ? t('saving') : t('save')} ); }