|
1 | 1 | import React, { useCallback } from 'react'; |
2 | | -import { ArrowRight, Shield } from 'lucide-react'; |
| 2 | +import { Shield, ArrowRight, Info } from 'lucide-react'; |
3 | 3 | import type { Delegation } from '@domain/identity/models/delegation.model'; |
4 | 4 | import { StatusBadge } from '@shared/components/StatusBadge'; |
5 | 5 | import { CodeBadge } from '@shared/components/CodeBadge'; |
6 | 6 | import { EntityRow } from '@shared/components/EntityRow'; |
7 | 7 | import { useI18n } from '@app/i18n/use-i18n'; |
8 | 8 | import { useStatusLabel } from '@app/hooks/use-status-label'; |
9 | | - |
| 9 | +import { |
| 10 | + M3DataView, |
| 11 | + SortOption, |
| 12 | + FilterOption, |
| 13 | + QueryCriteriaOption, |
| 14 | +} from '@shared/components/M3DataView'; |
| 15 | +import { ApiErrorBanner } from '@shared/components/ApiErrorBanner'; |
| 16 | +import { DelegationViewType } from '@app/identity/hooks/use-delegation-dashboard'; |
| 17 | +import { Tooltip } from '@shared/components/Tooltip'; |
10 | 18 |
|
11 | 19 | interface DelegationListPanelProps { |
12 | 20 | delegations: Delegation[]; |
13 | 21 | selectedId: string; |
14 | 22 | isLoading: boolean; |
15 | 23 | error: Error | null; |
16 | | - title: string; |
17 | | - emptyLabel: string; |
| 24 | + viewMode: 'list' | 'thumbnail'; |
| 25 | + onViewModeChange: (mode: 'list' | 'thumbnail') => void; |
| 26 | + searchCriteria: string; |
| 27 | + onSearchCriteriaChange: (criteria: string) => void; |
| 28 | + searchValue: string; |
| 29 | + onSearchValueChange: (value: string) => void; |
| 30 | + onSearchSubmit: (event: React.FormEvent) => void; |
| 31 | + onRegisterNew: () => void; |
| 32 | + sortBy: string; |
| 33 | + onSortByChange: (value: string) => void; |
| 34 | + sortOrder: 'asc' | 'desc'; |
| 35 | + onSortOrderToggle: () => void; |
| 36 | + activeFilter: string; |
| 37 | + onFilterChange: (value: string) => void; |
| 38 | + page: number; |
| 39 | + pageSize: number; |
| 40 | + totalItems: number; |
| 41 | + totalPages: number; |
| 42 | + startIndex: number; |
| 43 | + appliedTerm: string; |
| 44 | + onPageChange: (page: number) => void; |
| 45 | + onResetQuery: () => void; |
18 | 46 | onSelectDelegation: (delegationId: string) => void; |
19 | | - onRegisterNew?: () => void; |
| 47 | + criteriaOptions: QueryCriteriaOption[]; |
| 48 | + filterOptions: FilterOption[]; |
| 49 | + sortOptions: SortOption[]; |
| 50 | + delegationViewType: DelegationViewType; |
| 51 | + onDelegationViewTypeChange: (type: DelegationViewType) => void; |
20 | 52 | } |
21 | 53 |
|
22 | | - |
23 | | - |
24 | 54 | export const DelegationListPanel: React.FC<DelegationListPanelProps> = ({ |
25 | 55 | delegations, |
26 | 56 | selectedId, |
27 | 57 | isLoading, |
28 | 58 | error, |
29 | | - title, |
30 | | - emptyLabel, |
31 | | - onSelectDelegation, |
| 59 | + viewMode, |
| 60 | + onViewModeChange, |
| 61 | + searchCriteria, |
| 62 | + onSearchCriteriaChange, |
| 63 | + searchValue, |
| 64 | + onSearchValueChange, |
| 65 | + onSearchSubmit, |
32 | 66 | onRegisterNew, |
| 67 | + sortBy, |
| 68 | + onSortByChange, |
| 69 | + sortOrder, |
| 70 | + onSortOrderToggle, |
| 71 | + activeFilter, |
| 72 | + onFilterChange, |
| 73 | + page, |
| 74 | + pageSize, |
| 75 | + totalItems, |
| 76 | + totalPages, |
| 77 | + startIndex, |
| 78 | + appliedTerm, |
| 79 | + onPageChange, |
| 80 | + onResetQuery, |
| 81 | + onSelectDelegation, |
| 82 | + criteriaOptions, |
| 83 | + filterOptions, |
| 84 | + sortOptions, |
| 85 | + delegationViewType, |
| 86 | + onDelegationViewTypeChange, |
33 | 87 | }) => { |
34 | 88 | const t = useI18n(); |
35 | 89 | const getStatusLabel = useStatusLabel(); |
@@ -67,40 +121,120 @@ export const DelegationListPanel: React.FC<DelegationListPanelProps> = ({ |
67 | 121 | ); |
68 | 122 | }, [selectedId, onSelectDelegation, getStatusLabel]); |
69 | 123 |
|
| 124 | + const renderDelegationCard = useCallback((delegation: Delegation) => { |
| 125 | + const isSelected = delegation.delegationId === selectedId; |
| 126 | + return ( |
| 127 | + <div |
| 128 | + key={delegation.delegationId} |
| 129 | + onClick={() => onSelectDelegation(delegation.delegationId)} |
| 130 | + className={`p-4 rounded-xl border cursor-pointer transition-colors ${ |
| 131 | + isSelected |
| 132 | + ? 'border-m3-primary bg-m3-primary/5' |
| 133 | + : 'border-m3-outline/20 bg-m3-surface hover:bg-m3-surface-container' |
| 134 | + }`} |
| 135 | + > |
| 136 | + <div className="flex items-start justify-between mb-3"> |
| 137 | + <div className="flex items-center gap-2"> |
| 138 | + <Shield className={`w-5 h-5 ${isSelected ? 'text-m3-primary' : 'text-m3-secondary'}`} /> |
| 139 | + <span className="text-sm font-medium">{delegation.delegationId.substring(0, 8)}...</span> |
| 140 | + </div> |
| 141 | + <StatusBadge status={delegation.status} label={getStatusLabel(delegation.status)} /> |
| 142 | + </div> |
| 143 | + <div className="flex items-center gap-2 mt-2"> |
| 144 | + <span className="text-xs text-m3-on-surface-variant">Scope:</span> |
| 145 | + <CodeBadge code={delegation.scopeType} /> |
| 146 | + </div> |
| 147 | + </div> |
| 148 | + ); |
| 149 | + }, [selectedId, onSelectDelegation, getStatusLabel]); |
| 150 | + |
| 151 | + const footerTelemetry = ( |
| 152 | + <div className="flex items-center gap-3"> |
| 153 | + <div className="flex items-center gap-1.5"> |
| 154 | + <span className="h-2 w-2 rounded-full bg-m3-primary animate-pulse" /> |
| 155 | + <span className="text-xs font-medium text-m3-secondary/80"> |
| 156 | + {t.showing ?? 'Showing'} {totalItems === 0 ? 0 : startIndex + 1}-{Math.min(startIndex + pageSize, totalItems)} {t.of ?? 'of'} {totalItems} Delegations |
| 157 | + </span> |
| 158 | + </div> |
| 159 | + {appliedTerm.trim() && ( |
| 160 | + <button onClick={onResetQuery} className="text-xs font-medium text-rose-500 hover:underline flex items-center gap-1"> |
| 161 | + <Info className="w-3 h-3" /> {t.clearFilter ?? 'Clear Filters'} |
| 162 | + </button> |
| 163 | + )} |
| 164 | + </div> |
| 165 | + ); |
| 166 | + |
70 | 167 | return ( |
71 | 168 | <div className="flex flex-col h-full"> |
72 | | - <div className="flex items-center justify-between px-4 py-3 border-b border-m3-outline/10"> |
73 | | - <h2 className="text-sm font-semibold text-m3-on-surface">{title}</h2> |
74 | | - {onRegisterNew && ( |
75 | | - <button |
76 | | - onClick={onRegisterNew} |
77 | | - className="text-xs text-m3-primary hover:underline" |
78 | | - type="button" |
| 169 | + <div className="flex border-b border-m3-outline/15 px-4 pt-2 mb-2 bg-m3-surface-container/10"> |
| 170 | + <Tooltip content="Delegaciones que has recibido (donde tú eres el administrador delegado)."> |
| 171 | + <button |
| 172 | + onClick={() => onDelegationViewTypeChange('received')} |
| 173 | + className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${delegationViewType === 'received' ? 'border-m3-primary text-m3-primary' : 'border-transparent text-m3-on-surface-variant hover:text-m3-on-surface'}`} |
79 | 174 | > |
80 | | - {t.registerNew ?? 'New'} |
| 175 | + Received Delegations |
81 | 176 | </button> |
82 | | - )} |
| 177 | + </Tooltip> |
| 178 | + <Tooltip content="Delegaciones que has otorgado a otros (donde tú eres el administrador delegador)."> |
| 179 | + <button |
| 180 | + onClick={() => onDelegationViewTypeChange('granted')} |
| 181 | + className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${delegationViewType === 'granted' ? 'border-m3-primary text-m3-primary' : 'border-transparent text-m3-on-surface-variant hover:text-m3-on-surface'}`} |
| 182 | + > |
| 183 | + Granted Delegations |
| 184 | + </button> |
| 185 | + </Tooltip> |
83 | 186 | </div> |
84 | | - |
85 | | - <div className="flex-1 overflow-y-auto px-2 py-2"> |
86 | | - {isLoading && ( |
87 | | - <p className="text-xs text-m3-secondary px-2 py-4 text-center"> |
88 | | - {t.loadingAccounts ?? 'Loading...'} |
89 | | - </p> |
90 | | - )} |
91 | | - |
92 | | - {!isLoading && error && <ErrorBanner error={error} />} |
93 | | - |
94 | | - {!isLoading && !error && delegations.length === 0 && ( |
95 | | - <p className="text-xs text-m3-secondary px-2 py-4 text-center">{emptyLabel}</p> |
| 187 | + <M3DataView |
| 188 | + title={'Delegation Management'} |
| 189 | + subtitle={delegationViewType === 'received' ? 'Delegations granted to you' : 'Delegations you have granted to others'} |
| 190 | + searchPlaceholder={t.searchPlaceholder ?? 'Search...'} |
| 191 | + searchCriteria={criteriaOptions} |
| 192 | + activeCriteria={searchCriteria} |
| 193 | + onCriteriaChange={onSearchCriteriaChange} |
| 194 | + searchValue={searchValue} |
| 195 | + onSearchValueChange={onSearchValueChange} |
| 196 | + onSearchSubmit={onSearchSubmit} |
| 197 | + onRegisterNew={onRegisterNew} |
| 198 | + registerLabel={t.newBtn ?? 'New'} |
| 199 | + viewMode={viewMode} |
| 200 | + onViewModeChange={onViewModeChange} |
| 201 | + sortOptions={sortOptions} |
| 202 | + sortBy={sortBy} |
| 203 | + onSortByChange={onSortByChange} |
| 204 | + sortOrder={sortOrder} |
| 205 | + onSortOrderToggle={onSortOrderToggle} |
| 206 | + filterOptions={filterOptions} |
| 207 | + activeFilter={activeFilter} |
| 208 | + onFilterChange={onFilterChange} |
| 209 | + isLoading={isLoading} |
| 210 | + isEmpty={totalItems === 0} |
| 211 | + emptyLabel={t.noRecords ?? 'No records found'} |
| 212 | + emptyTitle={t.dataViewEmptyTitle ?? 'No Results'} |
| 213 | + loadingLabel={t.dataViewLoading ?? 'Loading...'} |
| 214 | + criteriaLabel={t.dataViewCriteriaLabel ?? 'Search by'} |
| 215 | + searchTermLabel={t.dataViewSearchTermLabel ?? 'Search term'} |
| 216 | + searchButtonLabel={t.dataViewSearchBtn ?? 'Search'} |
| 217 | + renderList={() => ( |
| 218 | + <> |
| 219 | + {error && <ApiErrorBanner error={error} />} |
| 220 | + <div className="overflow-x-auto border border-m3-outline/25 rounded-xl bg-m3-surface-container/20"> |
| 221 | + <div className="divide-y divide-m3-outline/10 text-sm"> |
| 222 | + {delegations.map(renderDelegationRow)} |
| 223 | + </div> |
| 224 | + </div> |
| 225 | + </> |
96 | 226 | )} |
97 | | - |
98 | | - {!isLoading && delegations.length > 0 && ( |
99 | | - <div className="divide-y divide-m3-outline/10"> |
100 | | - {delegations.map(renderDelegationRow)} |
101 | | - </div> |
| 227 | + renderThumbnail={() => ( |
| 228 | + <> |
| 229 | + {error && <ApiErrorBanner error={error} />} |
| 230 | + <div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> |
| 231 | + {delegations.map(renderDelegationCard)} |
| 232 | + </div> |
| 233 | + </> |
102 | 234 | )} |
103 | | - </div> |
| 235 | + pagination={{ page, pageSize, totalItems, totalPages, onPageChange }} |
| 236 | + telemetryInfo={footerTelemetry} |
| 237 | + /> |
104 | 238 | </div> |
105 | 239 | ); |
106 | 240 | }; |
0 commit comments