From ec9ff3e6335abe61347c343a75ebe8e81f77dfe3 Mon Sep 17 00:00:00 2001 From: Apple Date: Sat, 29 Nov 2025 16:41:28 -0800 Subject: [PATCH] feat(governance): Migrate Governance Engine to Next.js (apps/web) BREAKING: Replace old MicroDAO voting with Agent Governance Engine ## New Files - apps/web/src/lib/types/governance.ts - apps/web/src/lib/api/governance.ts - apps/web/src/lib/api/audit.ts - apps/web/src/lib/api/incidents.ts - apps/web/src/components/governance/GovernanceLevelBadge.tsx - apps/web/src/components/governance/ReportButton.tsx - apps/web/src/components/governance/CityGovernancePanel.tsx - apps/web/src/components/governance/AuditDashboard.tsx - apps/web/src/components/governance/IncidentsList.tsx - apps/web/src/app/audit/page.tsx - apps/web/src/app/incidents/page.tsx ## Updated Files - apps/web/src/app/governance/page.tsx - New City Governance UI - apps/web/src/components/Navigation.tsx - Shield icon for Governance ## Task docs/tasks/TASK_PHASE_GOVERNANCE_MIGRATION_NEXTJS.md --- apps/web/src/app/audit/page.tsx | 15 + apps/web/src/app/governance/page.tsx | 200 +- apps/web/src/app/incidents/page.tsx | 15 + apps/web/src/components/Navigation.tsx | 4 +- .../components/governance/AuditDashboard.tsx | 188 + .../governance/CityGovernancePanel.tsx | 242 + .../governance/GovernanceLevelBadge.tsx | 135 + .../components/governance/IncidentsList.tsx | 279 + .../components/governance/ReportButton.tsx | 226 + apps/web/src/components/governance/index.ts | 11 + apps/web/src/lib/api/audit.ts | 98 + apps/web/src/lib/api/governance.ts | 177 + apps/web/src/lib/api/incidents.ts | 161 + apps/web/src/lib/types/governance.ts | 302 + apps/web/src/lib/types/index.ts | 3 + .../TASK_PHASE_GOVERNANCE_MIGRATION_NEXTJS.md | 183 + .../deps_temp_9c9ec632/chunk-REFQX4J5.js | 1908 -- .../deps_temp_9c9ec632/chunk-REFQX4J5.js.map | 7 - .../.vite/deps_temp_9c9ec632/package.json | 3 - .../.vite/deps_temp_9c9ec632/react-dom.js | 21623 ---------------- .../.vite/deps_temp_9c9ec632/react-dom.js.map | 7 - .../.vite/deps_temp_9c9ec632/react.js | 5 - .../.vite/deps_temp_9c9ec632/react.js.map | 7 - .../react_jsx-dev-runtime.js | 911 - .../react_jsx-dev-runtime.js.map | 7 - .../deps_temp_9c9ec632/react_jsx-runtime.js | 923 - .../react_jsx-runtime.js.map | 7 - 27 files changed, 2042 insertions(+), 25605 deletions(-) create mode 100644 apps/web/src/app/audit/page.tsx create mode 100644 apps/web/src/app/incidents/page.tsx create mode 100644 apps/web/src/components/governance/AuditDashboard.tsx create mode 100644 apps/web/src/components/governance/CityGovernancePanel.tsx create mode 100644 apps/web/src/components/governance/GovernanceLevelBadge.tsx create mode 100644 apps/web/src/components/governance/IncidentsList.tsx create mode 100644 apps/web/src/components/governance/ReportButton.tsx create mode 100644 apps/web/src/components/governance/index.ts create mode 100644 apps/web/src/lib/api/audit.ts create mode 100644 apps/web/src/lib/api/governance.ts create mode 100644 apps/web/src/lib/api/incidents.ts create mode 100644 apps/web/src/lib/types/governance.ts create mode 100644 docs/tasks/TASK_PHASE_GOVERNANCE_MIGRATION_NEXTJS.md delete mode 100644 node_modules/.vite/deps_temp_9c9ec632/chunk-REFQX4J5.js delete mode 100644 node_modules/.vite/deps_temp_9c9ec632/chunk-REFQX4J5.js.map delete mode 100644 node_modules/.vite/deps_temp_9c9ec632/package.json delete mode 100644 node_modules/.vite/deps_temp_9c9ec632/react-dom.js delete mode 100644 node_modules/.vite/deps_temp_9c9ec632/react-dom.js.map delete mode 100644 node_modules/.vite/deps_temp_9c9ec632/react.js delete mode 100644 node_modules/.vite/deps_temp_9c9ec632/react.js.map delete mode 100644 node_modules/.vite/deps_temp_9c9ec632/react_jsx-dev-runtime.js delete mode 100644 node_modules/.vite/deps_temp_9c9ec632/react_jsx-dev-runtime.js.map delete mode 100644 node_modules/.vite/deps_temp_9c9ec632/react_jsx-runtime.js delete mode 100644 node_modules/.vite/deps_temp_9c9ec632/react_jsx-runtime.js.map diff --git a/apps/web/src/app/audit/page.tsx b/apps/web/src/app/audit/page.tsx new file mode 100644 index 00000000..adef5481 --- /dev/null +++ b/apps/web/src/app/audit/page.tsx @@ -0,0 +1,15 @@ +import { AuditDashboard } from '@/components/governance/AuditDashboard'; + +// Force dynamic rendering +export const dynamic = 'force-dynamic'; + +export default function AuditPage() { + return ( +
+
+ +
+
+ ); +} + diff --git a/apps/web/src/app/governance/page.tsx b/apps/web/src/app/governance/page.tsx index 2a90ab89..2ecf3ba8 100644 --- a/apps/web/src/app/governance/page.tsx +++ b/apps/web/src/app/governance/page.tsx @@ -1,204 +1,14 @@ -import Link from 'next/link' -import { Wallet, Users, Vote, FileText, TrendingUp, Shield, ArrowRight } from 'lucide-react' -import { api, MicroDAO } from '@/lib/api' -import { cn } from '@/lib/utils' +import { CityGovernancePanel } from '@/components/governance/CityGovernancePanel'; // Force dynamic rendering -export const dynamic = 'force-dynamic' - -async function getMicroDAOs(): Promise { - try { - return await api.getMicroDAOs() - } catch (error) { - console.error('Failed to fetch MicroDAOs:', error) - return [] - } -} - -export default async function GovernancePage() { - const daos = await getMicroDAOs() +export const dynamic = 'force-dynamic'; +export default function GovernancePage() { return (
- {/* Header */} -
-
-
- -
-
-

Governance

-

MicroDAO управління та голосування

-
-
-
- - {/* Stats Overview */} -
- - - - -
- - {/* MicroDAOs List */} -
-

- - Ваші MicroDAO -

- - {daos.length === 0 ? ( -
- -

- MicroDAO не знайдено -

-

- Ви ще не є учасником жодного MicroDAO. -

- -
- ) : ( -
- {daos.map((dao) => ( - - ))} -
- )} -
- - {/* Quick Actions */} -
-

Швидкі дії

- -
- - - -
-
+
- ) + ); } - -function StatCard({ - icon: Icon, - label, - value, - color -}: { - icon: React.ComponentType<{ className?: string }> - label: string - value: string - color: 'amber' | 'cyan' | 'violet' | 'emerald' -}) { - const colorClasses = { - amber: 'text-amber-400', - cyan: 'text-cyan-400', - violet: 'text-violet-400', - emerald: 'text-emerald-400' - } - - return ( -
- -
{value}
-
{label}
-
- ) -} - -function DAOCard({ dao }: { dao: MicroDAO }) { - return ( - -
-
-
- -
-
-

- {dao.name} -

-

{dao.slug}

-
-
- - - {dao.is_active ? 'Активний' : 'Неактивний'} - -
- -

- {dao.description || 'Без опису'} -

- -
-
- - - 0 учасників - - - - 0 пропозицій - -
- - -
- - ) -} - -function ActionCard({ - icon: Icon, - title, - description, - href -}: { - icon: React.ComponentType<{ className?: string }> - title: string - description: string - href: string -}) { - return ( - - -

{title}

-

{description}

- - ) -} - diff --git a/apps/web/src/app/incidents/page.tsx b/apps/web/src/app/incidents/page.tsx new file mode 100644 index 00000000..d60b41be --- /dev/null +++ b/apps/web/src/app/incidents/page.tsx @@ -0,0 +1,15 @@ +import { IncidentsList } from '@/components/governance/IncidentsList'; + +// Force dynamic rendering +export const dynamic = 'force-dynamic'; + +export default function IncidentsPage() { + return ( +
+
+ +
+
+ ); +} + diff --git a/apps/web/src/components/Navigation.tsx b/apps/web/src/components/Navigation.tsx index cd9252f0..aa4423b4 100644 --- a/apps/web/src/components/Navigation.tsx +++ b/apps/web/src/components/Navigation.tsx @@ -3,7 +3,7 @@ import { useState } from 'react' import Link from 'next/link' import { usePathname, useRouter } from 'next/navigation' -import { Menu, X, Home, Building2, User, Sparkles, Bot, Wallet, LogOut, Loader2, Server, Users, Network } from 'lucide-react' +import { Menu, X, Home, Building2, User, Sparkles, Bot, Wallet, LogOut, Loader2, Server, Users, Network, Shield } from 'lucide-react' import { cn } from '@/lib/utils' import { useAuth } from '@/context/AuthContext' @@ -13,7 +13,7 @@ const navItems = [ { href: '/citizens', label: 'Громадяни', icon: Users }, { href: '/agents', label: 'Агенти', icon: Bot }, { href: '/microdao', label: 'MicroDAO', icon: Network }, - { href: '/governance', label: 'DAO', icon: Wallet }, + { href: '/governance', label: 'Governance', icon: Shield }, { href: '/secondme', label: 'Second Me', icon: User }, { href: '/nodes', label: 'Ноди', icon: Server }, ] diff --git a/apps/web/src/components/governance/AuditDashboard.tsx b/apps/web/src/components/governance/AuditDashboard.tsx new file mode 100644 index 00000000..7774fd05 --- /dev/null +++ b/apps/web/src/components/governance/AuditDashboard.tsx @@ -0,0 +1,188 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { FileText, Calendar, User, Target, Filter, Loader2, RefreshCw } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { auditApi } from '@/lib/api/audit'; +import type { GovernanceEvent, GovernanceEventType, AuditStats } from '@/lib/types/governance'; + +const EVENT_TYPE_LABELS: Partial> = { + 'agent.promoted': 'Агент підвищений', + 'agent.demoted': 'Агент понижений', + 'agent.revoked': 'Агент заблокований', + 'agent.reinstated': 'Агент відновлений', + 'incident.created': 'Інцидент створено', + 'incident.resolved': 'Інцидент вирішено', + 'microdao.created': 'MicroDAO створено', + 'node.registered': 'Ноду зареєстровано', + 'room.created': 'Кімнату створено', +}; + +const EVENT_TYPE_COLORS: Partial> = { + 'agent.promoted': 'text-green-400 bg-green-500/20', + 'agent.demoted': 'text-orange-400 bg-orange-500/20', + 'agent.revoked': 'text-red-400 bg-red-500/20', + 'agent.reinstated': 'text-blue-400 bg-blue-500/20', + 'incident.created': 'text-yellow-400 bg-yellow-500/20', + 'incident.resolved': 'text-cyan-400 bg-cyan-500/20', + 'microdao.created': 'text-purple-400 bg-purple-500/20', + 'node.registered': 'text-pink-400 bg-pink-500/20', + 'room.created': 'text-amber-400 bg-amber-500/20', +}; + +export function AuditDashboard() { + const [events, setEvents] = useState([]); + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + const [eventTypeFilter, setEventTypeFilter] = useState(''); + + const loadData = async () => { + setLoading(true); + try { + const [eventsData, statsData] = await Promise.all([ + auditApi.getAuditEvents({ + limit: 50, + eventType: eventTypeFilter as GovernanceEventType || undefined, + }), + auditApi.getAuditStats(), + ]); + setEvents(eventsData); + setStats(statsData); + } catch (error) { + console.error('Failed to load audit data:', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadData(); + }, [eventTypeFilter]); + + const formatDate = (date: string) => { + return new Date(date).toLocaleString('uk-UA', { + day: '2-digit', + month: '2-digit', + hour: '2-digit', + minute: '2-digit', + }); + }; + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

Audit Dashboard

+

Журнал подій governance

+
+
+ +
+ + {/* Stats */} + {stats && ( +
+
+
{stats.totalEvents}
+
Всього подій
+
+
+
+ {Object.keys(stats.eventsByType).length} +
+
Типів подій
+
+
+
{stats.topActors.length}
+
Активних акторів
+
+
+
+ {stats.eventsByDay.slice(-7).reduce((sum, d) => sum + d.count, 0)} +
+
За 7 днів
+
+
+ )} + + {/* Filter */} +
+ + +
+ + {/* Events List */} + {loading ? ( +
+ +
+ ) : events.length === 0 ? ( +
+ +

Немає подій

+

Журнал подій порожній

+
+ ) : ( +
+ {events.map((event) => ( +
+
+ {/* Event Type Badge */} + + {EVENT_TYPE_LABELS[event.eventType] || event.eventType} + + + {/* Event Details */} +
+
+ + + {event.actorId} + + + + {event.targetId} + +
+
+ {event.scope} • {event.subject} +
+
+ + {/* Timestamp */} +
+ + {formatDate(event.createdAt)} +
+
+
+ ))} +
+ )} +
+ ); +} + diff --git a/apps/web/src/components/governance/CityGovernancePanel.tsx b/apps/web/src/components/governance/CityGovernancePanel.tsx new file mode 100644 index 00000000..87047043 --- /dev/null +++ b/apps/web/src/components/governance/CityGovernancePanel.tsx @@ -0,0 +1,242 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { + Landmark, Shield, Users, AlertTriangle, FileText, + ChevronRight, Loader2, RefreshCw +} from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { GovernanceLevelBadge } from './GovernanceLevelBadge'; +import { governanceApi } from '@/lib/api/governance'; +import { incidentsApi } from '@/lib/api/incidents'; +import type { GovernanceAgent, Incident } from '@/lib/types/governance'; + +export function CityGovernancePanel() { + const [cityAgents, setCityAgents] = useState([]); + const [districtLeads, setDistrictLeads] = useState([]); + const [openIncidents, setOpenIncidents] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const loadData = async () => { + setLoading(true); + setError(null); + + try { + const [agents, leads, incidents] = await Promise.all([ + governanceApi.getCityAgents().catch(() => []), + governanceApi.getDistrictLeadAgents().catch(() => []), + incidentsApi.getIncidents({ + escalationLevel: 'city', + status: 'open', + limit: 5 + }).catch(() => []), + ]); + + setCityAgents(agents); + setDistrictLeads(leads); + setOpenIncidents(incidents); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load data'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadData(); + }, []); + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

City Governance

+

DAARION.city управління

+
+
+ +
+ + {error && ( +
+ {error} +
+ )} + + {/* City Agents Grid */} +
+

+ + City Governance Agents +

+ + {cityAgents.length === 0 ? ( +
+ +

Немає city governance агентів

+
+ ) : ( +
+ {cityAgents.map((agent) => ( + +
+
+ {agent.avatarUrl ? ( + {agent.displayName} + ) : ( + agent.displayName.charAt(0).toUpperCase() + )} +
+
+

+ {agent.displayName} +

+ +
+
+ + ))} +
+ )} +
+ + {/* District Leads */} +
+

+ + District Lead Agents +

+ + {districtLeads.length === 0 ? ( +
+ +

Немає district lead агентів

+
+ ) : ( +
+ {districtLeads.map((agent) => ( + +
+
+ {agent.displayName.charAt(0).toUpperCase()} +
+
+

+ {agent.displayName} +

+ {agent.homeMicrodaoName && ( +

{agent.homeMicrodaoName}

+ )} +
+
+ + + ))} +
+ )} +
+ + {/* City-Level Incidents */} +
+
+

+ + City-Level Incidents +

+ + Всі інциденти + + +
+ + {openIncidents.length === 0 ? ( +
+ +

Немає відкритих інцидентів рівня City

+
+ ) : ( +
+ {openIncidents.map((incident) => ( + +
+
+

{incident.title}

+

+ {incident.description || 'Без опису'} +

+
+ + {incident.priority} + +
+ + ))} +
+ )} +
+ + {/* Quick Links */} +
+ + +

Audit Dashboard

+

Переглянути журнал подій governance

+ + + +

Incidents

+

Управління інцидентами та скаргами

+ +
+
+ ); +} + diff --git a/apps/web/src/components/governance/GovernanceLevelBadge.tsx b/apps/web/src/components/governance/GovernanceLevelBadge.tsx new file mode 100644 index 00000000..7086e3d3 --- /dev/null +++ b/apps/web/src/components/governance/GovernanceLevelBadge.tsx @@ -0,0 +1,135 @@ +'use client'; + +import { Shield, User, Users, Briefcase, Star, Crown, Building2, Landmark } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import type { AgentGovLevel, AgentStatus } from '@/lib/types/governance'; +import { GOV_LEVEL_LABELS, AGENT_STATUS_LABELS } from '@/lib/types/governance'; + +interface GovernanceLevelBadgeProps { + level: AgentGovLevel; + status?: AgentStatus; + showLabel?: boolean; + size?: 'sm' | 'md' | 'lg'; + className?: string; +} + +const LEVEL_CONFIG: Record; + bg: string; + text: string; + border: string; +}> = { + guest: { + icon: User, + bg: 'bg-slate-500/20', + text: 'text-slate-400', + border: 'border-slate-500/30', + }, + personal: { + icon: User, + bg: 'bg-blue-500/20', + text: 'text-blue-400', + border: 'border-blue-500/30', + }, + member: { + icon: Users, + bg: 'bg-green-500/20', + text: 'text-green-400', + border: 'border-green-500/30', + }, + worker: { + icon: Briefcase, + bg: 'bg-yellow-500/20', + text: 'text-yellow-400', + border: 'border-yellow-500/30', + }, + core_team: { + icon: Star, + bg: 'bg-orange-500/20', + text: 'text-orange-400', + border: 'border-orange-500/30', + }, + orchestrator: { + icon: Crown, + bg: 'bg-purple-500/20', + text: 'text-purple-400', + border: 'border-purple-500/30', + }, + district_lead: { + icon: Building2, + bg: 'bg-pink-500/20', + text: 'text-pink-400', + border: 'border-pink-500/30', + }, + city_governance: { + icon: Landmark, + bg: 'bg-red-500/20', + text: 'text-red-400', + border: 'border-red-500/30', + }, +}; + +const SIZE_CLASSES = { + sm: { + container: 'px-2 py-0.5 text-xs gap-1', + icon: 'w-3 h-3', + }, + md: { + container: 'px-3 py-1 text-sm gap-1.5', + icon: 'w-4 h-4', + }, + lg: { + container: 'px-4 py-2 text-base gap-2', + icon: 'w-5 h-5', + }, +}; + +export function GovernanceLevelBadge({ + level, + status = 'active', + showLabel = true, + size = 'md', + className, +}: GovernanceLevelBadgeProps) { + const config = LEVEL_CONFIG[level]; + const sizeClasses = SIZE_CLASSES[size]; + const Icon = config.icon; + + const isInactive = status !== 'active'; + + return ( +
+ {/* Level Badge */} + + + {showLabel && ( + + {GOV_LEVEL_LABELS[level]} + + )} + + + {/* Status Badge (if not active) */} + {isInactive && ( + + {AGENT_STATUS_LABELS[status]} + + )} +
+ ); +} + diff --git a/apps/web/src/components/governance/IncidentsList.tsx b/apps/web/src/components/governance/IncidentsList.tsx new file mode 100644 index 00000000..24ef35fa --- /dev/null +++ b/apps/web/src/components/governance/IncidentsList.tsx @@ -0,0 +1,279 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { + AlertTriangle, Clock, CheckCircle2, XCircle, + Filter, Loader2, RefreshCw, ChevronDown, ChevronUp, + ArrowUpRight, User +} from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { incidentsApi, type IncidentsFilter } from '@/lib/api/incidents'; +import type { + Incident, IncidentStatus, IncidentPriority, EscalationLevel +} from '@/lib/types/governance'; +import { + INCIDENT_STATUS_LABELS, + INCIDENT_PRIORITY_LABELS, + ESCALATION_LABELS, +} from '@/lib/types/governance'; + +const STATUS_ICONS: Record> = { + open: AlertTriangle, + in_progress: Clock, + resolved: CheckCircle2, + closed: XCircle, +}; + +const STATUS_COLORS: Record = { + open: 'text-yellow-400 bg-yellow-500/20', + in_progress: 'text-blue-400 bg-blue-500/20', + resolved: 'text-green-400 bg-green-500/20', + closed: 'text-slate-400 bg-slate-500/20', +}; + +const PRIORITY_COLORS: Record = { + low: 'text-slate-400 bg-slate-500/20', + medium: 'text-yellow-400 bg-yellow-500/20', + high: 'text-orange-400 bg-orange-500/20', + critical: 'text-red-400 bg-red-500/20', +}; + +export function IncidentsList() { + const [incidents, setIncidents] = useState([]); + const [loading, setLoading] = useState(true); + const [expandedId, setExpandedId] = useState(null); + const [filter, setFilter] = useState({ + limit: 50, + }); + + const loadData = async () => { + setLoading(true); + try { + const data = await incidentsApi.getIncidents(filter); + setIncidents(data); + } catch (error) { + console.error('Failed to load incidents:', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadData(); + }, [filter]); + + const formatDate = (date: string) => { + return new Date(date).toLocaleString('uk-UA', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + }; + + const handleEscalate = async (incident: Incident) => { + const nextLevel: EscalationLevel = + incident.escalationLevel === 'microdao' ? 'district' : + incident.escalationLevel === 'district' ? 'city' : 'city'; + + try { + await incidentsApi.escalateIncident( + incident.id, + nextLevel, + 'dais-demo-user', // TODO: real auth + 'Ескалація з UI' + ); + loadData(); + } catch (error) { + console.error('Failed to escalate:', error); + } + }; + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

Incidents

+

Управління інцидентами

+
+
+ +
+ + {/* Filters */} +
+
+ + +
+ + + + +
+ + {/* Incidents List */} + {loading ? ( +
+ +
+ ) : incidents.length === 0 ? ( +
+ +

Немає інцидентів

+

Список інцидентів порожній

+
+ ) : ( +
+ {incidents.map((incident) => { + const StatusIcon = STATUS_ICONS[incident.status]; + const isExpanded = expandedId === incident.id; + + return ( +
+ {/* Main Row */} +
setExpandedId(isExpanded ? null : incident.id)} + > +
+ {/* Status Icon */} +
+ +
+ + {/* Content */} +
+
+

{incident.title}

+ + {INCIDENT_PRIORITY_LABELS[incident.priority]} + +
+
+ {incident.targetScopeType}: {incident.targetScopeId} + + + {ESCALATION_LABELS[incident.escalationLevel]} + +
+
+ + {/* Expand */} +
+ {formatDate(incident.createdAt)} + {isExpanded ? ( + + ) : ( + + )} +
+
+
+ + {/* Expanded Details */} + {isExpanded && ( +
+ {incident.description && ( +

{incident.description}

+ )} + +
+ + + Створив: {incident.createdByDaisId} + + {incident.assignedToDaisId && ( + + + Призначено: {incident.assignedToDaisId} + + )} +
+ + {incident.resolution && ( +
+

+ Рішення: {incident.resolution} +

+
+ )} + + {/* Actions */} + {incident.status === 'open' && incident.escalationLevel !== 'city' && ( +
+ +
+ )} +
+ )} +
+ ); + })} +
+ )} +
+ ); +} + diff --git a/apps/web/src/components/governance/ReportButton.tsx b/apps/web/src/components/governance/ReportButton.tsx new file mode 100644 index 00000000..38015698 --- /dev/null +++ b/apps/web/src/components/governance/ReportButton.tsx @@ -0,0 +1,226 @@ +'use client'; + +import { useState } from 'react'; +import { AlertTriangle, X, Send, Loader2 } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { incidentsApi } from '@/lib/api/incidents'; +import type { TargetScopeType, IncidentPriority } from '@/lib/types/governance'; +import { INCIDENT_PRIORITY_LABELS } from '@/lib/types/governance'; + +interface ReportButtonProps { + targetScopeType: TargetScopeType; + targetScopeId: string; + actorDaisId: string; + variant?: 'icon' | 'button' | 'text'; + className?: string; +} + +export function ReportButton({ + targetScopeType, + targetScopeId, + actorDaisId, + variant = 'button', + className, +}: ReportButtonProps) { + const [isOpen, setIsOpen] = useState(false); + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [priority, setPriority] = useState('medium'); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!title.trim()) { + setError('Введіть заголовок'); + return; + } + + setIsSubmitting(true); + setError(null); + + try { + await incidentsApi.createIncident({ + createdByDaisId: actorDaisId, + targetScopeType, + targetScopeId, + priority, + title: title.trim(), + description: description.trim() || undefined, + }); + + setSuccess(true); + setTimeout(() => { + setIsOpen(false); + setTitle(''); + setDescription(''); + setPriority('medium'); + setSuccess(false); + }, 2000); + } catch (err) { + setError(err instanceof Error ? err.message : 'Помилка створення інциденту'); + } finally { + setIsSubmitting(false); + } + }; + + const buttonContent = { + icon: , + button: ( + <> + + Поскаржитись + + ), + text: Поскаржитись, + }; + + const buttonClasses = { + icon: 'p-2 rounded-lg hover:bg-red-500/20 text-slate-400 hover:text-red-400 transition-colors', + button: 'flex items-center gap-2 px-3 py-1.5 rounded-lg bg-red-500/10 text-red-400 hover:bg-red-500/20 transition-colors text-sm', + text: 'text-sm hover:underline', + }; + + return ( + <> + + + {/* Modal */} + {isOpen && ( +
+
+ {/* Header */} +
+
+
+ +
+
+

Створити інцидент

+

+ {targetScopeType}: {targetScopeId} +

+
+
+ +
+ + {success ? ( +
+
+ +
+

Інцидент створено!

+

Ваша скарга буде розглянута модераторами

+
+ ) : ( +
+ {/* Title */} +
+ + setTitle(e.target.value)} + placeholder="Коротко опишіть проблему" + className="w-full px-4 py-2 bg-slate-800/50 border border-slate-700 rounded-lg text-white placeholder:text-slate-500 focus:border-red-500 focus:outline-none" + maxLength={100} + /> +
+ + {/* Priority */} +
+ +
+ {(['low', 'medium', 'high', 'critical'] as IncidentPriority[]).map((p) => ( + + ))} +
+
+ + {/* Description */} +
+ +