feat: visually distinguish digital-twin linked tenants
All checks were successful
Deploy to Production / deploy (push) Successful in 2m2s

- Add has_digital_twin flag to tenant API response
- Show cloud badge on tenant card for linked tenants
- Hide sync/import buttons for tenants without mapping
- Add .tenant-badge-dt CSS for the badge
This commit is contained in:
Johngreen
2026-02-12 14:41:58 +09:00
parent 66018e37c4
commit fcd0b78f3c
5 changed files with 42 additions and 9 deletions

View File

@@ -2,7 +2,7 @@
import { useState, useCallback, useMemo } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { useMachines } from '@/lib/hooks';
import { useMachines, useTenants } from '@/lib/hooks';
import { useToast } from '@/lib/toast-context';
import { api } from '@/lib/api';
import { MachineList } from '@/components/MachineList';
@@ -36,7 +36,10 @@ export default function TenantDashboard() {
const router = useRouter();
const tenantId = params?.tenant as string;
const { machines, isLoading, error, mutate } = useMachines(tenantId);
const { tenants } = useTenants();
const { addToast } = useToast();
const currentTenant = tenants.find(t => t.id === tenantId);
const hasDigitalTwin = currentTenant?.has_digital_twin ?? false;
const [showModal, setShowModal] = useState(false);
const [form, setForm] = useState<MachineForm>(INITIAL_FORM);
@@ -270,14 +273,18 @@ export default function TenantDashboard() {
</h2>
<div className="page-header-actions">
<button className={`btn-sync ${syncing ? 'sync-spinning' : ''}`} onClick={handleSync} disabled={syncing}>
<span className="material-symbols-outlined">sync</span>
</button>
<button className="btn-outline" onClick={openImportModal}>
<span className="material-symbols-outlined">cloud_download</span>
</button>
{hasDigitalTwin && (
<>
<button className={`btn-sync ${syncing ? 'sync-spinning' : ''}`} onClick={handleSync} disabled={syncing}>
<span className="material-symbols-outlined">sync</span>
</button>
<button className="btn-outline" onClick={openImportModal}>
<span className="material-symbols-outlined">cloud_download</span>
</button>
</>
)}
<button className="btn-primary" onClick={openCreate}>
<span className="material-symbols-outlined">add</span>

View File

@@ -425,6 +425,23 @@ a {
color: var(--md-on-surface-variant);
}
.tenant-badge-dt {
display: inline-flex;
align-items: center;
gap: 4px;
margin-top: 4px;
padding: 3px 10px;
border-radius: 12px;
background: var(--md-primary-container);
color: var(--md-on-primary-container);
font-size: 11px;
font-weight: 500;
}
.tenant-badge-dt .material-symbols-outlined {
font-size: 14px;
}
/* ===== Login ===== */
.login-container {
min-height: 100vh;

View File

@@ -49,6 +49,12 @@ export default function HomePage() {
<span className="material-symbols-outlined tenant-icon">factory</span>
<span className="tenant-name">{tenant.name}</span>
<span className="tenant-id">{tenant.id}</span>
{tenant.has_digital_twin && (
<span className="tenant-badge-dt">
<span className="material-symbols-outlined">cloud</span>
</span>
)}
</button>
))}
</div>

View File

@@ -12,6 +12,7 @@ export interface Tenant {
name: string;
industry_type: string;
is_active: boolean;
has_digital_twin: boolean;
created_at: string | null;
}

View File

@@ -44,6 +44,7 @@ async def get_tenant(db: AsyncSession, tenant_id: str) -> Dict:
"name": str(tenant.name),
"industry_type": str(tenant.industry_type),
"is_active": bool(tenant.is_active),
"has_digital_twin": bool(tenant.digital_twin_company_id),
"created_at": _format_timestamp(tenant.created_at),
}
@@ -99,6 +100,7 @@ async def list_tenants(db: AsyncSession) -> List[Dict]:
"name": str(t.name),
"industry_type": str(t.industry_type),
"is_active": bool(t.is_active),
"has_digital_twin": bool(t.digital_twin_company_id),
"created_at": _format_timestamp(t.created_at),
}
for t in tenants