mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
feat(onboarding/phase-6): setup wizard polish, seed script alignment, doctor visit slots
- Setup wizard: 3-pane layout with right-side live previews, resume banner, edit/copy icons on team step, AI prompt configuration - Forms: employee-create replaces invite-member (no email invites), clinic form with address/hours/payment, doctor form with visit slots - Seed script: aligned to current SDK schema — doctors created as workspace members (HelixEngage Manager role), visitingHours replaced by doctorVisitSlot entity, clinics seeded, portalUserId linked dynamically, SUB/ORIGIN/GQL configurable via env vars - Pages: clinics + doctors CRUD updated for new schema, team settings with temp password + role assignment - New components: time-picker, day-selector, wizard-right-panes, wizard-layout-context, resume-setup-banner - Removed: invite-member-form (replaced by employee-create-form per no-email-invites rule) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,23 +1,21 @@
|
||||
/**
|
||||
* Helix Engage — Platform Data Seeder
|
||||
* Creates 5 patient stories + 5 doctors with fully linked records.
|
||||
* Run: cd helix-engage && npx tsx scripts/seed-data.ts
|
||||
* Creates 2 clinics, 5 doctors with multi-clinic visit slots,
|
||||
* 3 patient stories with fully linked records (campaigns, leads,
|
||||
* calls, appointments, follow-ups, lead activities).
|
||||
*
|
||||
* Platform field mapping (SDK name → platform name):
|
||||
* Campaign: campaignType→typeCustom, campaignStatus→status, impressionCount→impressions,
|
||||
* clickCount→clicks, contactedCount→contacted, convertedCount→converted, leadCount→leadsGenerated
|
||||
* Lead: leadSource→source, leadStatus→status, firstContactedAt→firstContacted,
|
||||
* lastContactedAt→lastContacted, landingPageUrl→landingPage
|
||||
* Call: callDirection→direction, durationSeconds→durationSec
|
||||
* Appointment: durationMinutes→durationMin, appointmentStatus→status, roomNumber→room
|
||||
* FollowUp: followUpType→typeCustom, followUpStatus→status
|
||||
* Patient: address→addressCustom
|
||||
* Doctor: isActive→active, branch→branchClinic
|
||||
* Run: cd helix-engage && npx tsx scripts/seed-data.ts
|
||||
* Env: SEED_GQL (graphql url), SEED_ORIGIN (workspace origin), SEED_SUB (workspace subdomain)
|
||||
*
|
||||
* Schema alignment (2026-04-10):
|
||||
* - Doctor.visitingHours removed → replaced by DoctorVisitSlot entity
|
||||
* - Doctor.portalUserId omitted (workspace member IDs are per-deployment)
|
||||
* - Clinic entity added (needed for visit slot FK)
|
||||
* NOTE: callNotes/visitNotes/clinicalNotes are RICH_TEXT — read-only, cannot seed
|
||||
*/
|
||||
|
||||
const GQL = process.env.SEED_GQL ?? 'http://localhost:4000/graphql';
|
||||
const SUB = 'fortytwo-dev';
|
||||
const SUB = process.env.SEED_SUB ?? 'fortytwo-dev';
|
||||
const ORIGIN = process.env.SEED_ORIGIN ?? 'http://fortytwo-dev.localhost:4010';
|
||||
|
||||
let token = '';
|
||||
@@ -51,28 +49,119 @@ async function mk(entity: string, data: any): Promise<string> {
|
||||
return d[`create${cap}`].id;
|
||||
}
|
||||
|
||||
// Create a workspace member (user account) and return its workspace member id.
|
||||
// Uses signUpInWorkspace + updateWorkspaceMember for name + updateWorkspaceMemberRole.
|
||||
// The invite hash and role IDs are fetched once and cached.
|
||||
let _inviteHash = '';
|
||||
let _wsId = '';
|
||||
const _roleIds: Record<string, string> = {};
|
||||
|
||||
async function ensureWorkspaceContext() {
|
||||
if (_wsId) return;
|
||||
const ws = await gql('{ currentWorkspace { id inviteHash } }');
|
||||
_wsId = ws.currentWorkspace.id;
|
||||
_inviteHash = ws.currentWorkspace.inviteHash;
|
||||
const roles = await gql('{ getRoles { id label } }');
|
||||
for (const r of roles.getRoles) _roleIds[r.label] = r.id;
|
||||
}
|
||||
|
||||
async function mkMember(email: string, password: string, firstName: string, lastName: string, roleName?: string): Promise<string> {
|
||||
await ensureWorkspaceContext();
|
||||
|
||||
// Create the user + link to workspace
|
||||
await gql(
|
||||
`mutation($email: String!, $password: String!, $workspaceId: UUID!, $workspaceInviteHash: String!) {
|
||||
signUpInWorkspace(email: $email, password: $password, workspaceId: $workspaceId, workspaceInviteHash: $workspaceInviteHash) { workspace { id } }
|
||||
}`,
|
||||
{ email, password, workspaceId: _wsId, workspaceInviteHash: _inviteHash },
|
||||
);
|
||||
|
||||
// Find the new member id
|
||||
const members = await gql('{ workspaceMembers { edges { node { id userEmail } } } }');
|
||||
const member = members.workspaceMembers.edges.find((e: any) => e.node.userEmail.toLowerCase() === email.toLowerCase());
|
||||
if (!member) throw new Error(`Could not find workspace member for ${email}`);
|
||||
const memberId = member.node.id;
|
||||
|
||||
// Set their display name
|
||||
await gql(
|
||||
`mutation($id: UUID!, $data: WorkspaceMemberUpdateInput!) { updateWorkspaceMember(id: $id, data: $data) { id } }`,
|
||||
{ id: memberId, data: { name: { firstName, lastName } } },
|
||||
);
|
||||
|
||||
// Assign role if specified
|
||||
if (roleName && _roleIds[roleName]) {
|
||||
await gql(
|
||||
`mutation($wm: UUID!, $role: UUID!) { updateWorkspaceMemberRole(workspaceMemberId: $wm, roleId: $role) { id } }`,
|
||||
{ wm: memberId, role: _roleIds[roleName] },
|
||||
);
|
||||
}
|
||||
|
||||
return memberId;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🌱 Seeding Helix Engage demo data...\n');
|
||||
await auth();
|
||||
console.log('✅ Auth OK\n');
|
||||
|
||||
// Workspace member IDs — switch based on target platform
|
||||
const WM = GQL.includes('srv1477139') ? {
|
||||
drSharma: '107efa70-fd32-4819-8936-994197c6ada1',
|
||||
drPatel: '7e1fe368-1f23-4a10-8c2f-3e9c3846b209',
|
||||
drKumar: 'b86ff7d3-57de-44e5-aa13-e5da848a960c',
|
||||
drReddy: 'b82693b6-701c-4783-8d02-cc137c9c306b',
|
||||
drSingh: 'b2a00dd2-5bb5-4c29-8fb1-70a681193a4c',
|
||||
} : {
|
||||
drSharma: '251e9b32-3a83-4f3c-a904-fad7e8b840c3',
|
||||
drPatel: '2b1bbf20-3838-434f-9fe9-b98436362230',
|
||||
drKumar: '16109622-9b13-4682-b327-eb611ffa8338',
|
||||
drReddy: '478a9ccb-d231-48fb-a740-0228d3c9325b',
|
||||
drSingh: 'b854b55b-7302-4981-8dfc-bea516abdc86',
|
||||
};
|
||||
// ═══════════════════════════════════════════
|
||||
// CLINICS (needed for doctor visit slots)
|
||||
// ═══════════════════════════════════════════
|
||||
console.log('🏥 Clinics');
|
||||
const clinicKor = await mk('clinic', {
|
||||
name: 'Global Hospital — Koramangala',
|
||||
clinicName: 'Global Hospital — Koramangala',
|
||||
status: 'ACTIVE',
|
||||
opensAt: '08:00', closesAt: '20:00',
|
||||
openMonday: true, openTuesday: true, openWednesday: true,
|
||||
openThursday: true, openFriday: true, openSaturday: true, openSunday: false,
|
||||
phone: { primaryPhoneNumber: '8041763265', primaryPhoneCallingCode: '+91', primaryPhoneCountryCode: 'IN' },
|
||||
addressCustom: { addressCity: 'Bangalore', addressState: 'Karnataka', addressCountry: 'India', addressStreet1: 'Koramangala 4th Block' },
|
||||
onlineBooking: true, walkInAllowed: true, acceptsCash: 'YES', acceptsCard: 'YES', acceptsUpi: 'YES',
|
||||
});
|
||||
console.log(` Koramangala: ${clinicKor}`);
|
||||
|
||||
const clinicWf = await mk('clinic', {
|
||||
name: 'Global Hospital — Whitefield',
|
||||
clinicName: 'Global Hospital — Whitefield',
|
||||
status: 'ACTIVE',
|
||||
opensAt: '09:00', closesAt: '18:00',
|
||||
openMonday: true, openTuesday: true, openWednesday: true,
|
||||
openThursday: true, openFriday: true, openSaturday: true, openSunday: false,
|
||||
phone: { primaryPhoneNumber: '8041763400', primaryPhoneCallingCode: '+91', primaryPhoneCountryCode: 'IN' },
|
||||
addressCustom: { addressCity: 'Bangalore', addressState: 'Karnataka', addressCountry: 'India', addressStreet1: 'ITPL Main Road, Whitefield' },
|
||||
onlineBooking: true, walkInAllowed: true, acceptsCash: 'YES', acceptsCard: 'YES', acceptsUpi: 'YES',
|
||||
});
|
||||
console.log(` Whitefield: ${clinicWf}\n`);
|
||||
|
||||
await auth();
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// DOCTORS (linked to workspace members)
|
||||
// DOCTOR WORKSPACE MEMBERS
|
||||
//
|
||||
// Each doctor gets a real platform login so they can access the
|
||||
// portal. Created via signUpInWorkspace, then linked to the Doctor
|
||||
// entity via portalUserId. Email domain matches the deployment.
|
||||
// ═══════════════════════════════════════════
|
||||
console.log('👤 Doctor workspace members (role: HelixEngage Manager)');
|
||||
const wmSharma = await mkMember('dr.sharma@globalcare.com', 'DrSharma@2026', 'Arun', 'Sharma', 'HelixEngage Manager');
|
||||
console.log(` Dr. Sharma member: ${wmSharma}`);
|
||||
const wmPatel = await mkMember('dr.patel@globalcare.com', 'DrPatel@2026', 'Meena', 'Patel', 'HelixEngage Manager');
|
||||
console.log(` Dr. Patel member: ${wmPatel}`);
|
||||
const wmKumar = await mkMember('dr.kumar@globalcare.com', 'DrKumar@2026', 'Rajesh', 'Kumar', 'HelixEngage Manager');
|
||||
console.log(` Dr. Kumar member: ${wmKumar}`);
|
||||
const wmReddy = await mkMember('dr.reddy@globalcare.com', 'DrReddy@2026', 'Lakshmi', 'Reddy', 'HelixEngage Manager');
|
||||
console.log(` Dr. Reddy member: ${wmReddy}`);
|
||||
const wmSingh = await mkMember('dr.singh@globalcare.com', 'DrSingh@2026', 'Harpreet', 'Singh', 'HelixEngage Manager');
|
||||
console.log(` Dr. Singh member: ${wmSingh}\n`);
|
||||
|
||||
await auth();
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// DOCTORS (linked to workspace members via portalUserId)
|
||||
//
|
||||
// visitingHours was removed — multi-clinic schedules now live
|
||||
// on DoctorVisitSlot (seeded below).
|
||||
// ═══════════════════════════════════════════
|
||||
console.log('👨⚕️ Doctors');
|
||||
const drSharma = await mk('doctor', {
|
||||
@@ -82,16 +171,15 @@ async function main() {
|
||||
specialty: 'Interventional Cardiology',
|
||||
qualifications: 'MBBS, MD (Medicine), DM (Cardiology), FACC',
|
||||
yearsOfExperience: 18,
|
||||
visitingHours: 'Mon/Wed/Fri 10:00 AM – 1:00 PM',
|
||||
consultationFeeNew: { amountMicros: 800_000_000, currencyCode: 'INR' },
|
||||
consultationFeeFollowUp: { amountMicros: 500_000_000, currencyCode: 'INR' },
|
||||
phone: { primaryPhoneNumber: '9900100001', primaryPhoneCallingCode: '+91', primaryPhoneCountryCode: 'IN' },
|
||||
email: { primaryEmail: 'dr.sharma@globalhospital.com' },
|
||||
email: { primaryEmail: 'dr.sharma@globalcare.com' },
|
||||
registrationNumber: 'KMC-45672',
|
||||
active: true,
|
||||
portalUserId: WM.drSharma,
|
||||
portalUserId: wmSharma,
|
||||
});
|
||||
console.log(` Dr. Sharma (Cardiology, WM: ${WM.drSharma}): ${drSharma}`);
|
||||
console.log(` Dr. Sharma (Cardiology → ${wmSharma}): ${drSharma}`);
|
||||
|
||||
const drPatel = await mk('doctor', {
|
||||
name: 'Dr. Meena Patel',
|
||||
@@ -100,16 +188,15 @@ async function main() {
|
||||
specialty: 'Reproductive Medicine & IVF',
|
||||
qualifications: 'MBBS, MS (OBG), Fellowship in Reproductive Medicine',
|
||||
yearsOfExperience: 15,
|
||||
visitingHours: 'Tue/Thu/Sat 9:00 AM – 12:00 PM',
|
||||
consultationFeeNew: { amountMicros: 700_000_000, currencyCode: 'INR' },
|
||||
consultationFeeFollowUp: { amountMicros: 400_000_000, currencyCode: 'INR' },
|
||||
phone: { primaryPhoneNumber: '9900100002', primaryPhoneCallingCode: '+91', primaryPhoneCountryCode: 'IN' },
|
||||
email: { primaryEmail: 'dr.patel@globalhospital.com' },
|
||||
email: { primaryEmail: 'dr.patel@globalcare.com' },
|
||||
registrationNumber: 'KMC-38291',
|
||||
active: true,
|
||||
portalUserId: WM.drPatel,
|
||||
portalUserId: wmPatel,
|
||||
});
|
||||
console.log(` Dr. Patel (Gynecology/IVF, WM: ${WM.drPatel}): ${drPatel}`);
|
||||
console.log(` Dr. Patel (Gynecology/IVF → ${wmPatel}): ${drPatel}`);
|
||||
|
||||
const drKumar = await mk('doctor', {
|
||||
name: 'Dr. Rajesh Kumar',
|
||||
@@ -118,16 +205,15 @@ async function main() {
|
||||
specialty: 'Joint Replacement & Sports Medicine',
|
||||
qualifications: 'MBBS, MS (Ortho), Fellowship in Arthroplasty',
|
||||
yearsOfExperience: 12,
|
||||
visitingHours: 'Mon–Fri 2:00 PM – 5:00 PM',
|
||||
consultationFeeNew: { amountMicros: 600_000_000, currencyCode: 'INR' },
|
||||
consultationFeeFollowUp: { amountMicros: 400_000_000, currencyCode: 'INR' },
|
||||
phone: { primaryPhoneNumber: '9900100003', primaryPhoneCallingCode: '+91', primaryPhoneCountryCode: 'IN' },
|
||||
email: { primaryEmail: 'dr.kumar@globalhospital.com' },
|
||||
email: { primaryEmail: 'dr.kumar@globalcare.com' },
|
||||
registrationNumber: 'KMC-51003',
|
||||
active: true,
|
||||
portalUserId: WM.drKumar,
|
||||
portalUserId: wmKumar,
|
||||
});
|
||||
console.log(` Dr. Kumar (Orthopedics, WM: ${WM.drKumar}): ${drKumar}`);
|
||||
console.log(` Dr. Kumar (Orthopedics → ${wmKumar}): ${drKumar}`);
|
||||
|
||||
const drReddy = await mk('doctor', {
|
||||
name: 'Dr. Lakshmi Reddy',
|
||||
@@ -136,16 +222,15 @@ async function main() {
|
||||
specialty: 'Internal Medicine & Preventive Health',
|
||||
qualifications: 'MBBS, MD (General Medicine)',
|
||||
yearsOfExperience: 20,
|
||||
visitingHours: 'Mon–Sat 9:00 AM – 6:00 PM',
|
||||
consultationFeeNew: { amountMicros: 500_000_000, currencyCode: 'INR' },
|
||||
consultationFeeFollowUp: { amountMicros: 300_000_000, currencyCode: 'INR' },
|
||||
phone: { primaryPhoneNumber: '9900100004', primaryPhoneCallingCode: '+91', primaryPhoneCountryCode: 'IN' },
|
||||
email: { primaryEmail: 'dr.reddy@globalhospital.com' },
|
||||
email: { primaryEmail: 'dr.reddy@globalcare.com' },
|
||||
registrationNumber: 'KMC-22145',
|
||||
active: true,
|
||||
portalUserId: WM.drReddy,
|
||||
portalUserId: wmReddy,
|
||||
});
|
||||
console.log(` Dr. Reddy (General Medicine, WM: ${WM.drReddy}): ${drReddy}`);
|
||||
console.log(` Dr. Reddy (General Medicine → ${wmReddy}): ${drReddy}`);
|
||||
|
||||
const drSingh = await mk('doctor', {
|
||||
name: 'Dr. Harpreet Singh',
|
||||
@@ -154,16 +239,57 @@ async function main() {
|
||||
specialty: 'Otorhinolaryngology & Head/Neck Surgery',
|
||||
qualifications: 'MBBS, MS (ENT), DNB',
|
||||
yearsOfExperience: 10,
|
||||
visitingHours: 'Mon/Wed/Fri 11:00 AM – 3:00 PM',
|
||||
consultationFeeNew: { amountMicros: 600_000_000, currencyCode: 'INR' },
|
||||
consultationFeeFollowUp: { amountMicros: 400_000_000, currencyCode: 'INR' },
|
||||
phone: { primaryPhoneNumber: '9900100005', primaryPhoneCallingCode: '+91', primaryPhoneCountryCode: 'IN' },
|
||||
email: { primaryEmail: 'dr.singh@globalhospital.com' },
|
||||
email: { primaryEmail: 'dr.singh@globalcare.com' },
|
||||
registrationNumber: 'KMC-60782',
|
||||
active: true,
|
||||
portalUserId: WM.drSingh,
|
||||
portalUserId: wmSingh,
|
||||
});
|
||||
console.log(` Dr. Singh (ENT, WM: ${WM.drSingh}): ${drSingh}\n`);
|
||||
console.log(` Dr. Singh (ENT → ${wmSingh}): ${drSingh}\n`);
|
||||
|
||||
await auth();
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// DOCTOR VISIT SLOTS (weekly schedule per doctor × clinic)
|
||||
// ═══════════════════════════════════════════
|
||||
console.log('📅 Visit Slots');
|
||||
const slots: Array<{ doc: string; docName: string; clinic: string; clinicName: string; day: string; start: string; end: string }> = [
|
||||
// Dr. Sharma — Koramangala Mon/Wed/Fri 10:00–13:00
|
||||
{ doc: drSharma, docName: 'Sharma', clinic: clinicKor, clinicName: 'Kor', day: 'MONDAY', start: '10:00', end: '13:00' },
|
||||
{ doc: drSharma, docName: 'Sharma', clinic: clinicKor, clinicName: 'Kor', day: 'WEDNESDAY', start: '10:00', end: '13:00' },
|
||||
{ doc: drSharma, docName: 'Sharma', clinic: clinicKor, clinicName: 'Kor', day: 'FRIDAY', start: '10:00', end: '13:00' },
|
||||
// Dr. Patel — Whitefield Tue/Thu/Sat 9:00–12:00
|
||||
{ doc: drPatel, docName: 'Patel', clinic: clinicWf, clinicName: 'WF', day: 'TUESDAY', start: '09:00', end: '12:00' },
|
||||
{ doc: drPatel, docName: 'Patel', clinic: clinicWf, clinicName: 'WF', day: 'THURSDAY', start: '09:00', end: '12:00' },
|
||||
{ doc: drPatel, docName: 'Patel', clinic: clinicWf, clinicName: 'WF', day: 'SATURDAY', start: '09:00', end: '12:00' },
|
||||
// Dr. Kumar — Koramangala Mon–Fri 14:00–17:00
|
||||
{ doc: drKumar, docName: 'Kumar', clinic: clinicKor, clinicName: 'Kor', day: 'MONDAY', start: '14:00', end: '17:00' },
|
||||
{ doc: drKumar, docName: 'Kumar', clinic: clinicKor, clinicName: 'Kor', day: 'TUESDAY', start: '14:00', end: '17:00' },
|
||||
{ doc: drKumar, docName: 'Kumar', clinic: clinicKor, clinicName: 'Kor', day: 'WEDNESDAY', start: '14:00', end: '17:00' },
|
||||
{ doc: drKumar, docName: 'Kumar', clinic: clinicKor, clinicName: 'Kor', day: 'THURSDAY', start: '14:00', end: '17:00' },
|
||||
{ doc: drKumar, docName: 'Kumar', clinic: clinicKor, clinicName: 'Kor', day: 'FRIDAY', start: '14:00', end: '17:00' },
|
||||
// Dr. Reddy — both clinics Mon–Sat
|
||||
{ doc: drReddy, docName: 'Reddy', clinic: clinicKor, clinicName: 'Kor', day: 'MONDAY', start: '09:00', end: '13:00' },
|
||||
{ doc: drReddy, docName: 'Reddy', clinic: clinicKor, clinicName: 'Kor', day: 'WEDNESDAY', start: '09:00', end: '13:00' },
|
||||
{ doc: drReddy, docName: 'Reddy', clinic: clinicKor, clinicName: 'Kor', day: 'FRIDAY', start: '09:00', end: '13:00' },
|
||||
{ doc: drReddy, docName: 'Reddy', clinic: clinicWf, clinicName: 'WF', day: 'TUESDAY', start: '14:00', end: '18:00' },
|
||||
{ doc: drReddy, docName: 'Reddy', clinic: clinicWf, clinicName: 'WF', day: 'THURSDAY', start: '14:00', end: '18:00' },
|
||||
{ doc: drReddy, docName: 'Reddy', clinic: clinicWf, clinicName: 'WF', day: 'SATURDAY', start: '14:00', end: '18:00' },
|
||||
// Dr. Singh — Whitefield Mon/Wed/Fri 11:00–15:00
|
||||
{ doc: drSingh, docName: 'Singh', clinic: clinicWf, clinicName: 'WF', day: 'MONDAY', start: '11:00', end: '15:00' },
|
||||
{ doc: drSingh, docName: 'Singh', clinic: clinicWf, clinicName: 'WF', day: 'WEDNESDAY', start: '11:00', end: '15:00' },
|
||||
{ doc: drSingh, docName: 'Singh', clinic: clinicWf, clinicName: 'WF', day: 'FRIDAY', start: '11:00', end: '15:00' },
|
||||
];
|
||||
for (const s of slots) {
|
||||
await mk('doctorVisitSlot', {
|
||||
name: `Dr. ${s.docName} — ${s.day} ${s.start}–${s.end} (${s.clinicName})`,
|
||||
doctorId: s.doc, clinicId: s.clinic,
|
||||
dayOfWeek: s.day, startTime: s.start, endTime: s.end,
|
||||
});
|
||||
}
|
||||
console.log(` ${slots.length} visit slots created\n`);
|
||||
|
||||
await auth();
|
||||
|
||||
@@ -406,9 +532,10 @@ async function main() {
|
||||
console.log(' Vijay — appointment reminder (tomorrow 9am)\n');
|
||||
|
||||
console.log('🎉 Seed complete!');
|
||||
console.log(' 5 doctors · 3 campaigns · 3 patients · 5 leads · 6 appointments · 10 calls · 22 activities · 2 follow-ups');
|
||||
console.log(' 2 clinics · 5 doctors · 20 visit slots · 3 campaigns');
|
||||
console.log(' 3 patients · 5 leads · 6 appointments · 10 calls · 22 activities · 2 follow-ups');
|
||||
console.log(' Demo phones: Priya=9949879837, Ravi=6309248884');
|
||||
console.log(' All appointments linked to doctor entities');
|
||||
console.log(' Doctors linked to clinics via visit slots (multi-clinic schedule)');
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('💥', e.message); process.exit(1); });
|
||||
|
||||
Reference in New Issue
Block a user