refactor: update tenant dashboard UI with styled GradientStatCard components and refreshed layout header

This commit is contained in:
Yashwin 2026-05-11 13:12:08 +05:30
parent 2cfce21323
commit d36c34aa9f
3 changed files with 153 additions and 139 deletions

View File

@ -87,7 +87,7 @@ export const DataTable = <T,>({
return ( return (
<th <th
key={column.key} key={column.key}
className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2 md:py-1 lg:py-2.5 xl:py-3 ${alignClass} text-[10px] md:text-[8px] lg:text-xs font-medium text-[#9aa6b2] uppercase`} className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2 md:py-1 lg:py-2.5 xl:py-3 ${alignClass} text-[10px] md:text-[8px] lg:text-[13px] font-medium text-[#9aa6b2] uppercase`}
> >
{column.label} {column.label}
</th> </th>
@ -97,7 +97,7 @@ export const DataTable = <T,>({
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td colSpan={desktopColSpan} className="px-3 md:px-2 lg:px-4 xl:px-5 py-6 md:py-3 lg:py-7 xl:py-8 text-center text-xs md:text-xs lg:text-sm text-[#6b7280]"> <td colSpan={desktopColSpan} className="px-3 md:px-2 lg:px-4 xl:px-5 py-6 md:py-3 lg:py-7 xl:py-8 text-center text-xs md:text-xs lg:text-[13px] text-[#6b7280]">
{emptyMessage} {emptyMessage}
</td> </td>
</tr> </tr>
@ -134,7 +134,7 @@ export const DataTable = <T,>({
return ( return (
<th <th
key={column.key} key={column.key}
className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2 md:py-1 lg:py-2.5 xl:py-3 ${alignClass} text-[10px] md:text-[8px] lg:text-xs font-medium text-[#9aa6b2] uppercase`} className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2 md:py-1 lg:py-2.5 xl:py-3 ${alignClass} text-[10px] md:text-[8px] lg:text-[13px] font-medium text-[#9aa6b2] uppercase`}
> >
{column.label} {column.label}
</th> </th>
@ -175,7 +175,7 @@ export const DataTable = <T,>({
? 'text-center' ? 'text-center'
: 'text-left'; : 'text-left';
return ( return (
<td key={column.key} className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2.5 md:py-1.5 lg:py-3 xl:py-4 ${alignClass} text-xs md:text-[10px] lg:text-sm`}> <td key={column.key} className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2.5 md:py-1.5 lg:py-3 xl:py-4 ${alignClass} text-xs md:text-[10px] lg:text-[13px]`}>
{column.render ? column.render(item) : String((item as any)[column.key])} {column.render ? column.render(item) : String((item as any)[column.key])}
</td> </td>
); );

View File

@ -16,18 +16,18 @@ export const GradientStatCard: React.FC<GradientStatCardProps> = ({ icon: Icon,
return ( return (
<div <div
className="rounded-[8px] p-[1px] h-full" className="rounded-[8px] p-[1px] h-full"
style={{ style={{
background: 'var(--Linear, linear-gradient(161deg, #084CC8 -1.15%, #75C044 44.29%, #FED314 89.74%))', background: 'var(--Linear, linear-gradient(161deg, #084CC8 -1.15%, #75C044 44.29%, #FED314 89.74%))',
}} }}
> >
<div className="flex flex-col items-start gap-3 px-4 py-4 min-h-[108px] h-full w-full rounded-[7px] bg-[#F8FAFC]"> <div className="flex flex-col items-start gap-3 px-4 py-4 min-h-[108px] h-full w-full rounded-[7px] bg-white">
<div className="flex items-start justify-between w-full"> <div className="flex items-center justify-between w-full">
<div className="text-[#94A3B8]"> <div className="text-[#94A3B8]">
<Icon className="w-5 h-5 stroke-[1.8]" /> <Icon className="w-5 h-5 stroke-[1.8]" />
</div> </div>
{badge && ( {badge && (
<div className={cn( <div className={cn(
"px-2.5 py-1 rounded-full text-[10px] font-bold tracking-tight whitespace-nowrap", "px-2.5 py-1 rounded-full text-[12px] font-bold tracking-tight whitespace-nowrap",
(badge.variant === 'success' || badge.variant === 'green') ? "bg-[#f1fffb] text-[#16c784]" : (badge.variant === 'success' || badge.variant === 'green') ? "bg-[#f1fffb] text-[#16c784]" :
badge.variant === 'warning' ? "bg-[#fff5e5] text-[#fca004]" : badge.variant === 'warning' ? "bg-[#fff5e5] text-[#fca004]" :
badge.variant === 'info' ? "bg-[#f0f9ff] text-[#0ea5e9]" : badge.variant === 'info' ? "bg-[#f0f9ff] text-[#0ea5e9]" :
@ -40,10 +40,10 @@ export const GradientStatCard: React.FC<GradientStatCardProps> = ({ icon: Icon,
</div> </div>
<div> <div>
<h4 className="text-[28px] leading-none font-bold text-[#1E293B] tracking-[-0.02em]"> <h4 className="text-[24px] leading-none font-bold text-[#1E293B] tracking-[-0.02em]">
{value} {value}
</h4> </h4>
<p className="text-[11px] font-semibold text-[#64748B] uppercase tracking-wider mt-1"> <p className="text-[12px] font-semibold text-[#64748B] uppercase tracking-wider mt-1">
{label} {label}
</p> </p>
</div> </div>

View File

@ -14,6 +14,7 @@ import {
Building2, Building2,
BadgeCheck, BadgeCheck,
GitBranch, GitBranch,
Zap,
} from "lucide-react"; } from "lucide-react";
import { Layout } from "@/components/layout/Layout"; import { Layout } from "@/components/layout/Layout";
import { import {
@ -23,8 +24,14 @@ import {
SuppliersTable, SuppliersTable,
type Column, type Column,
PrimaryButton, PrimaryButton,
GradientStatCard,
} from "@/components/shared"; } from "@/components/shared";
import { UsersTable, RolesTable, DepartmentsTable, type DepartmentsTableRef } from "@/components/superadmin"; import {
UsersTable,
RolesTable,
DepartmentsTable,
type DepartmentsTableRef,
} from "@/components/superadmin";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
import { tenantService } from "@/services/tenant-service"; import { tenantService } from "@/services/tenant-service";
import { moduleService } from "@/services/module-service"; import { moduleService } from "@/services/module-service";
@ -210,41 +217,52 @@ const TenantDetails = (): ReactElement => {
]} ]}
pageHeader={{ pageHeader={{
title: tenant.name, title: tenant.name,
action: activeTab === "departments" ? ( action:
<PrimaryButton activeTab === "departments" ? (
onClick={() => departmentsRef.current?.openNewModal()} <PrimaryButton
className="flex items-center gap-2" onClick={() => departmentsRef.current?.openNewModal()}
> className="flex items-center gap-2"
<Plus className="w-4 h-4" /> >
<span>New Department</span> <Plus className="w-4 h-4" />
</PrimaryButton> <span>New Department</span>
) : undefined </PrimaryButton>
) : undefined,
}} }}
> >
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
{/* Tenant Header Card */} {/* Tenant Header Card */}
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg p-4 md:p-6"> <div className="flex items-start justify-between self-stretch rounded-[4px] border border-[#D1D5DB] bg-white px-3 py-4">
<div className="flex flex-col md:flex-row md:items-start md:justify-between gap-4"> {/* Left Section */}
<div className="flex items-start gap-4"> <div className="flex items-start gap-6">
<div className="w-12 h-12 md:w-16 md:h-16 bg-[#f5f7fa] border border-[rgba(0,0,0,0.08)] rounded-lg flex items-center justify-center shrink-0"> {/* Avatar */}
<span className="text-lg md:text-xl font-normal text-[#9aa6b2]"> <div className="flex h-16 w-16 shrink-0 items-center justify-center rounded-[8px] bg-[#F3F4F6]">
{getTenantInitials(tenant.name)} <span className="text-[32px] font-semibold leading-none text-[#111827]">
</span> {getTenantInitials(tenant.name)}
</span>
</div>
{/* Content */}
<div className="flex flex-col gap-2">
{/* Name + Status */}
<div className="flex items-center gap-3">
<h1 className="text-[30px] font-semibold leading-[36px] tracking-[-0.6px] text-[#111827]">
{tenant.name}
</h1>
<StatusBadge variant={getStatusVariant(tenant.status)}>
{tenant.status}
</StatusBadge>
</div> </div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2"> {/* Meta Information */}
<h1 className="text-xl md:text-2xl font-bold text-[#0f1724] tracking-[-0.48px] truncate"> <div className="flex flex-col gap-2 text-sm text-[#6B7280]">
{tenant.name} {/* First Row */}
</h1> <div className="flex flex-wrap items-center gap-6">
<StatusBadge variant={getStatusVariant(tenant.status)}>
{tenant.status}
</StatusBadge>
</div>
<div className="flex flex-wrap items-center gap-4 md:gap-6 text-sm font-normal text-[#6b7280]">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Hash className="w-4 h-4" /> <Hash className="h-4 w-4" />
<span className="truncate">{tenant.slug}</span> <span>ID: {tenant.slug}</span>
</div> </div>
{tenant.domain && ( {tenant.domain && (
<a <a
href={ href={
@ -254,31 +272,44 @@ const TenantDetails = (): ReactElement => {
} }
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-1.5 text-[#6b7280] hover:text-[#112868] transition-colors" className="flex items-center gap-1.5 hover:text-[#112868] transition-colors"
> >
<Globe className="w-4 h-4" /> <Globe className="h-4 w-4" />
<span className="truncate">{tenant.domain}</span> <span>{tenant.domain}</span>
</a> </a>
)} )}
<div className="flex items-center gap-1.5">
<Calendar className="w-4 h-4" />
<span>Created {formatDate(tenant.created_at)}</span>
</div>
</div> </div>
{/* Second Row */}
<div className="flex flex-wrap items-center gap-6">
<div className="flex items-center gap-1.5">
<Calendar className="h-4 w-4" />
<span>Created: {formatDate(tenant.created_at)}</span>
</div>
{/* {tenant.primary_contact && (
<div className="flex items-center gap-1.5">
<User className="h-4 w-4" />
<span>Admin: {tenant.primary_contact}</span>
</div>
)} */}
</div>
</div> </div>
</div> </div>
<button
onClick={() => navigate(`/tenants/${id}/edit`)}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-[#112868] bg-white border border-[rgba(0,0,0,0.08)] rounded-md hover:bg-gray-50 transition-colors"
>
<Edit className="w-4 h-4" />
<span>Edit Tenant</span>
</button>
</div> </div>
{/* Edit Button */}
<button
onClick={() => navigate(`/tenants/${id}/edit`)}
className="flex items-center gap-2 rounded-md border border-[#D1D5DB] bg-white px-4 py-2 text-sm font-medium text-[#112868] transition-colors hover:bg-[#F9FAFB]"
>
<Edit className="h-4 w-4" />
<span>Edit Tenant</span>
</button>
</div> </div>
{/* Tabs */} {/* Tabs */}
<div className="border border-[rgba(0,0,0,0.08)] rounded-lg bg-white"> <div >
<div className="border-b border-[rgba(0,0,0,0.08)] px-4 md:px-6"> <div className="border-b border-[rgba(0,0,0,0.08)] px-4 md:px-6">
<div className="flex gap-1 overflow-x-auto"> <div className="flex gap-1 overflow-x-auto">
{tabs.map((tab) => ( {tabs.map((tab) => (
@ -310,7 +341,11 @@ const TenantDetails = (): ReactElement => {
<RolesTable tenantId={id} compact={false} /> <RolesTable tenantId={id} compact={false} />
)} )}
{activeTab === "departments" && id && ( {activeTab === "departments" && id && (
<DepartmentsTable ref={departmentsRef} tenantId={id} compact={true} /> <DepartmentsTable
ref={departmentsRef}
tenantId={id}
compact={true}
/>
)} )}
{activeTab === "designations" && id && ( {activeTab === "designations" && id && (
<DesignationsTable tenantId={id} compact={false} /> <DesignationsTable tenantId={id} compact={false} />
@ -367,105 +402,85 @@ const OverviewTab = ({ tenant, stats }: OverviewTabProps): ReactElement => {
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
{/* Stats Cards */} {/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="bg-[#f5f7fa] border border-[rgba(0,0,0,0.08)] rounded-lg p-4"> <GradientStatCard
<div className="text-sm font-medium text-[#6b7280] mb-1"> icon={Users}
Total Users value={stats?.totalUsers || 0}
</div> label="Total Users"
<div className="text-2xl font-bold text-[#0f1724]"> />
{stats?.totalUsers || 0} <GradientStatCard
</div> icon={Package}
</div> value={stats?.totalModules || 0}
<div className="bg-[#f5f7fa] border border-[rgba(0,0,0,0.08)] rounded-lg p-4"> label="Total Modules"
<div className="text-sm font-medium text-[#6b7280] mb-1"> />
Total Modules <GradientStatCard
</div> icon={Zap}
<div className="text-2xl font-bold text-[#0f1724]"> value={stats?.activeModules || 0}
{stats?.totalModules || 0} label="Active Modules"
</div> badge={
</div> stats?.activeModules && stats.activeModules > 0
<div className="bg-[#f5f7fa] border border-[rgba(0,0,0,0.08)] rounded-lg p-4"> ? { text: "Running", variant: "success" }
<div className="text-sm font-medium text-[#6b7280] mb-1"> : undefined
Active Modules }
</div> />
<div className="text-2xl font-bold text-[#0f1724]"> <GradientStatCard
{stats?.activeModules || 0} icon={CreditCard}
</div> value={stats?.subscriptionTier || "N/A"}
</div> label="Subscription Tier"
<div className="bg-[#f5f7fa] border border-[rgba(0,0,0,0.08)] rounded-lg p-4"> badge={{ text: "Plan", variant: "info" }}
<div className="text-sm font-medium text-[#6b7280] mb-1"> />
Subscription Tier
</div>
<div className="text-2xl font-bold text-[#0f1724] capitalize">
{stats?.subscriptionTier || "N/A"}
</div>
</div>
</div> </div>
{/* General Information */} {/* General Information */}
<div className="bg-[#f5f7fa] border border-[rgba(0,0,0,0.08)] rounded-lg p-4 md:p-6"> <div className="flex flex-col items-start gap-[10px] rounded-[8px] border border-[#D1D5DB] bg-white p-4">
<h3 className="text-lg font-semibold text-[#0f1724] mb-4"> {/* Header Section */}
General Information <div className="flex w-full flex-col items-start border-b border-[#D1D5DB] pb-2">
</h3> <h3 className="text-[20px] font-semibold leading-[28px] text-[#111827]">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> General Information
<div> </h3>
<div className="text-sm font-medium text-[#6b7280] mb-1"> </div>
{/* Content Section */}
<div className="flex w-full flex-col items-start gap-[10px]">
<div className="grid w-full grid-cols-1 gap-y-4 md:grid-cols-[180px_1fr]">
{/* Tenant Name */}
<div className="text-sm font-medium text-[#6B7280]">
Tenant Name Tenant Name
</div> </div>
<div className="text-sm font-normal text-[#0f1724]"> <div className="text-sm font-normal text-[#111827]">
{tenant.name} {tenant.name}
</div> </div>
</div>
<div> {/* Slug */}
<div className="text-sm font-medium text-[#6b7280] mb-1">Slug</div> <div className="text-sm font-medium text-[#6B7280]">Slug</div>
<div className="text-sm font-normal text-[#0f1724]"> <div className="text-sm font-normal text-[#111827]">
{tenant.slug} {tenant.slug}
</div> </div>
</div>
<div> {/* Status */}
<div className="text-sm font-medium text-[#6b7280] mb-1"> <div className="text-sm font-medium text-[#6B7280]">Status</div>
Status <div>
<StatusBadge variant={getStatusVariant(tenant.status)}>
{tenant.status}
</StatusBadge>
</div> </div>
<StatusBadge variant={getStatusVariant(tenant.status)}>
{tenant.status} {/* Subscription Tier */}
</StatusBadge> <div className="text-sm font-medium text-[#6B7280]">
</div>
<div>
<div className="text-sm font-medium text-[#6b7280] mb-1">
Subscription Tier Subscription Tier
</div> </div>
<div className="text-sm font-normal text-[#0f1724] capitalize"> <div className="text-sm font-normal text-[#111827] capitalize">
{tenant.subscription_tier || "N/A"} {tenant.subscription_tier || "N/A"}
</div> </div>
</div>
{/* <div> {/* Created At */}
<div className="text-sm font-medium text-[#6b7280] mb-1"> <div className="text-sm font-medium text-[#6B7280]">Created At</div>
Max Users <div className="text-sm font-normal text-[#111827]">
</div>
<div className="text-sm font-normal text-[#0f1724]">
{tenant.max_users || "Unlimited"}
</div>
</div>
<div>
<div className="text-sm font-medium text-[#6b7280] mb-1">
Max Modules
</div>
<div className="text-sm font-normal text-[#0f1724]">
{tenant.max_modules || "Unlimited"}
</div>
</div> */}
<div>
<div className="text-sm font-medium text-[#6b7280] mb-1">
Created At
</div>
<div className="text-sm font-normal text-[#0f1724]">
{formatDate(tenant.created_at)} {formatDate(tenant.created_at)}
</div> </div>
</div>
<div> {/* Updated At */}
<div className="text-sm font-medium text-[#6b7280] mb-1"> <div className="text-sm font-medium text-[#6B7280]">Updated At</div>
Updated At <div className="text-sm font-normal text-[#111827]">
</div>
<div className="text-sm font-normal text-[#0f1724]">
{formatDate(tenant.updated_at)} {formatDate(tenant.updated_at)}
</div> </div>
</div> </div>
@ -644,7 +659,6 @@ const LicenseTab = ({ tenant: _tenant }: LicenseTabProps): ReactElement => {
); );
}; };
// Billing Tab Component // Billing Tab Component
interface BillingTabProps { interface BillingTabProps {
tenant: Tenant; tenant: Tenant;