improvement: account setting added to header in order to choose organizations/subscribtions/etc
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
// backend/prisma/seed.ts
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { config } from 'dotenv';
|
||||
import path from 'path';
|
||||
|
||||
@@ -29,14 +28,14 @@ async function main() {
|
||||
console.log('✅ Database connected successfully');
|
||||
|
||||
// Create organization types
|
||||
const clinicType = await prisma.organizationType.upsert({
|
||||
await prisma.organizationType.upsert({
|
||||
where: { name: 'CLINIC' },
|
||||
update: {},
|
||||
create: { name: 'CLINIC' },
|
||||
});
|
||||
console.log('✅ Created clinic type');
|
||||
|
||||
const labType = await prisma.organizationType.upsert({
|
||||
await prisma.organizationType.upsert({
|
||||
where: { name: 'LAB' },
|
||||
update: {},
|
||||
create: { name: 'LAB' },
|
||||
@@ -61,32 +60,39 @@ async function main() {
|
||||
}
|
||||
console.log('✅ Created plans');
|
||||
|
||||
// Create features and permissions
|
||||
// Minimal permission model (confirmed):
|
||||
// - Sidebar tabs use READ/EDIT
|
||||
// - EDIT implies READ in app logic
|
||||
// - Owners effectively get all permissions
|
||||
const features = [
|
||||
{
|
||||
name: 'Patient Management',
|
||||
permissions: ['VIEW_PATIENTS', 'CREATE_PATIENTS', 'EDIT_PATIENTS', 'DELETE_PATIENTS']
|
||||
name: 'Today',
|
||||
permissions: ['TAB_TODAY_READ', 'TAB_TODAY_EDIT'],
|
||||
},
|
||||
{
|
||||
name: 'Order Management',
|
||||
permissions: ['VIEW_ORDERS', 'CREATE_ORDERS', 'EDIT_ORDERS', 'DELETE_ORDERS', 'TRACK_ORDERS']
|
||||
name: 'Patients',
|
||||
permissions: ['TAB_PATIENTS_READ', 'TAB_PATIENTS_EDIT'],
|
||||
},
|
||||
{
|
||||
name: 'Case Management',
|
||||
permissions: ['VIEW_CASES', 'CREATE_CASES', 'EDIT_CASES', 'DELETE_CASES']
|
||||
name: 'Appointments',
|
||||
permissions: ['TAB_APPOINTMENTS_READ', 'TAB_APPOINTMENTS_EDIT'],
|
||||
},
|
||||
{
|
||||
name: 'Reports',
|
||||
permissions: ['VIEW_REPORTS', 'EXPORT_REPORTS']
|
||||
name: 'Staff Management',
|
||||
permissions: ['TAB_STAFF_READ', 'TAB_STAFF_EDIT'],
|
||||
},
|
||||
{
|
||||
name: 'Team Management',
|
||||
permissions: ['INVITE_USERS', 'REMOVE_USERS', 'MANAGE_PERMISSIONS']
|
||||
name: 'Lab Management',
|
||||
permissions: ['TAB_LAB_READ', 'TAB_LAB_EDIT'],
|
||||
},
|
||||
{
|
||||
name: 'Billing',
|
||||
permissions: ['VIEW_INVOICES', 'CREATE_INVOICES', 'MANAGE_PAYMENTS']
|
||||
}
|
||||
permissions: ['TAB_BILLING_READ', 'TAB_BILLING_EDIT'],
|
||||
},
|
||||
{
|
||||
name: 'Reports',
|
||||
permissions: ['TAB_REPORTS_READ', 'TAB_REPORTS_EDIT'],
|
||||
},
|
||||
];
|
||||
|
||||
for (const feature of features) {
|
||||
@@ -109,7 +115,7 @@ async function main() {
|
||||
}
|
||||
console.log('✅ Created features and permissions');
|
||||
|
||||
console.log('🌱 Seeding completed successfully!'); ``
|
||||
console.log('🌱 Seeding completed successfully!');
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
@@ -145,6 +145,20 @@ export class AuthController {
|
||||
return this.authService.getProfile(req.user.id);
|
||||
}
|
||||
|
||||
@Get('subscription-alert')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Owner-only: seat / trial status for warning indicator (current org from JWT)',
|
||||
})
|
||||
async getSubscriptionAlert(@Req() req) {
|
||||
return this.authService.getOwnerSubscriptionAlert(
|
||||
req.user.id,
|
||||
req.user.organizationId,
|
||||
);
|
||||
}
|
||||
|
||||
// =========================
|
||||
// LOGOUT
|
||||
// =========================
|
||||
|
||||
@@ -16,27 +16,20 @@ import { CreateOrganizationDto } from './dto/create-organization.dto';
|
||||
import { JwtPayload } from './interfaces/jwt-payload.interface';
|
||||
|
||||
const ALL_PERMISSIONS = [
|
||||
'VIEW_PATIENTS',
|
||||
'CREATE_PATIENTS',
|
||||
'EDIT_PATIENTS',
|
||||
'DELETE_PATIENTS',
|
||||
'VIEW_ORDERS',
|
||||
'CREATE_ORDERS',
|
||||
'EDIT_ORDERS',
|
||||
'DELETE_ORDERS',
|
||||
'TRACK_ORDERS',
|
||||
'VIEW_CASES',
|
||||
'CREATE_CASES',
|
||||
'EDIT_CASES',
|
||||
'DELETE_CASES',
|
||||
'VIEW_REPORTS',
|
||||
'EXPORT_REPORTS',
|
||||
'INVITE_USERS',
|
||||
'REMOVE_USERS',
|
||||
'MANAGE_PERMISSIONS',
|
||||
'VIEW_INVOICES',
|
||||
'CREATE_INVOICES',
|
||||
'MANAGE_PAYMENTS',
|
||||
'TAB_TODAY_READ',
|
||||
'TAB_TODAY_EDIT',
|
||||
'TAB_PATIENTS_READ',
|
||||
'TAB_PATIENTS_EDIT',
|
||||
'TAB_APPOINTMENTS_READ',
|
||||
'TAB_APPOINTMENTS_EDIT',
|
||||
'TAB_STAFF_READ',
|
||||
'TAB_STAFF_EDIT',
|
||||
'TAB_LAB_READ',
|
||||
'TAB_LAB_EDIT',
|
||||
'TAB_BILLING_READ',
|
||||
'TAB_BILLING_EDIT',
|
||||
'TAB_REPORTS_READ',
|
||||
'TAB_REPORTS_EDIT',
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
@@ -63,6 +56,7 @@ export class AuthService {
|
||||
organization: {
|
||||
include: {
|
||||
type: true, // Include organization type (CLINIC/LAB)
|
||||
plan: true,
|
||||
}
|
||||
},
|
||||
permissions: {
|
||||
@@ -149,6 +143,12 @@ export class AuthService {
|
||||
permissions: membership.isOwner
|
||||
? ALL_PERMISSIONS
|
||||
: membership.permissions?.map(p => p.permission.name) || [],
|
||||
plan: membership.organization.plan
|
||||
? {
|
||||
name: membership.organization.plan.name,
|
||||
maxUsers: membership.organization.plan.maxUsers,
|
||||
}
|
||||
: undefined,
|
||||
})) || [];
|
||||
|
||||
return {
|
||||
@@ -322,6 +322,7 @@ export class AuthService {
|
||||
organization: {
|
||||
include: {
|
||||
type: true,
|
||||
plan: true,
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
@@ -346,7 +347,15 @@ export class AuthService {
|
||||
name: membership.organization.name,
|
||||
type: membership.organization.type.name,
|
||||
isOwner: membership.isOwner,
|
||||
permissions: membership.permissions?.map(p => p.permission.name) || [],
|
||||
permissions: membership.isOwner
|
||||
? ALL_PERMISSIONS
|
||||
: membership.permissions?.map(p => p.permission.name) || [],
|
||||
plan: membership.organization.plan
|
||||
? {
|
||||
name: membership.organization.plan.name,
|
||||
maxUsers: membership.organization.plan.maxUsers,
|
||||
}
|
||||
: undefined,
|
||||
})) || [];
|
||||
|
||||
return {
|
||||
@@ -412,6 +421,7 @@ export class AuthService {
|
||||
organization: {
|
||||
include: {
|
||||
type: true,
|
||||
plan: true,
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
@@ -457,7 +467,15 @@ export class AuthService {
|
||||
name: membership.organization.name,
|
||||
type: membership.organization.type.name,
|
||||
isOwner: membership.isOwner,
|
||||
permissions: membership.permissions?.map(p => p.permission.name) || [],
|
||||
permissions: membership.isOwner
|
||||
? ALL_PERMISSIONS
|
||||
: membership.permissions?.map(p => p.permission.name) || [],
|
||||
plan: membership.organization.plan
|
||||
? {
|
||||
name: membership.organization.plan.name,
|
||||
maxUsers: membership.organization.plan.maxUsers,
|
||||
}
|
||||
: undefined,
|
||||
})) || [];
|
||||
|
||||
return {
|
||||
@@ -629,6 +647,7 @@ export class AuthService {
|
||||
organization: {
|
||||
include: {
|
||||
type: true,
|
||||
plan: true,
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
@@ -654,7 +673,15 @@ export class AuthService {
|
||||
name: membership.organization.name,
|
||||
type: membership.organization.type.name,
|
||||
isOwner: membership.isOwner,
|
||||
permissions: membership.permissions?.map(p => p.permission.name) || [],
|
||||
permissions: membership.isOwner
|
||||
? ALL_PERMISSIONS
|
||||
: membership.permissions?.map(p => p.permission.name) || [],
|
||||
plan: membership.organization.plan
|
||||
? {
|
||||
name: membership.organization.plan.name,
|
||||
maxUsers: membership.organization.plan.maxUsers,
|
||||
}
|
||||
: undefined,
|
||||
})) || [];
|
||||
|
||||
return {
|
||||
@@ -711,7 +738,9 @@ export class AuthService {
|
||||
});
|
||||
|
||||
// 4. Format permissions
|
||||
const permissions = membership.permissions.map(p => p.permission.name);
|
||||
const permissions = membership.isOwner
|
||||
? ALL_PERMISSIONS
|
||||
: membership.permissions.map(p => p.permission.name);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -721,9 +750,101 @@ export class AuthService {
|
||||
id: membership.organization.id,
|
||||
name: membership.organization.name,
|
||||
type: membership.organization.type.name,
|
||||
isOwner: membership.isOwner,
|
||||
plan: membership.organization.plan
|
||||
? {
|
||||
name: membership.organization.plan.name,
|
||||
maxUsers: membership.organization.plan.maxUsers,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
permissions,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Owner-only subscription / seat alerts for the current org (from JWT).
|
||||
* Used for a subtle warning indicator in the app shell (not staff-facing banners).
|
||||
*/
|
||||
async getOwnerSubscriptionAlert(userId: string, organizationId: string | undefined) {
|
||||
if (!organizationId) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
showWarning: false,
|
||||
seatsLow: false,
|
||||
trialEndingSoon: false,
|
||||
trialExpired: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const membership = await this.prisma.membership.findFirst({
|
||||
where: { userId, organizationId },
|
||||
include: {
|
||||
organization: {
|
||||
include: { plan: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!membership || !membership.isOwner) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
showWarning: false,
|
||||
seatsLow: false,
|
||||
trialEndingSoon: false,
|
||||
trialExpired: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const org = membership.organization;
|
||||
const plan = org.plan;
|
||||
const maxUsers = plan.maxUsers;
|
||||
const seatsUsed = await this.prisma.membership.count({
|
||||
where: { organizationId: org.id },
|
||||
});
|
||||
|
||||
const unlimited = maxUsers >= 999999;
|
||||
const remaining = unlimited ? Infinity : maxUsers - seatsUsed;
|
||||
const seatsLow =
|
||||
!unlimited && remaining >= 0 && remaining <= 2 && maxUsers > 0;
|
||||
|
||||
let trialEndingSoon = false;
|
||||
let trialExpired = false;
|
||||
let daysUntilTrialEnd: number | null = null;
|
||||
let trialEndsAt: string | null = null;
|
||||
|
||||
if (plan.name === 'trial') {
|
||||
const end = new Date(org.createdAt);
|
||||
end.setDate(end.getDate() + 30);
|
||||
trialEndsAt = end.toISOString();
|
||||
const ms = end.getTime() - Date.now();
|
||||
daysUntilTrialEnd = Math.ceil(ms / (1000 * 60 * 60 * 24));
|
||||
if (daysUntilTrialEnd <= 0) {
|
||||
trialExpired = true;
|
||||
} else if (daysUntilTrialEnd <= 7) {
|
||||
trialEndingSoon = true;
|
||||
}
|
||||
}
|
||||
|
||||
const showWarning = seatsLow || trialEndingSoon || trialExpired;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
showWarning,
|
||||
seatsLow,
|
||||
trialEndingSoon,
|
||||
trialExpired,
|
||||
seatsUsed,
|
||||
seatsLimit: maxUsers,
|
||||
daysUntilTrialEnd,
|
||||
trialEndsAt,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user