Added language selection and translation files
This commit is contained in:
4124
frontend/package-lock.json
generated
Normal file
4124
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,8 +10,11 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"i18next": "^25.7.1",
|
||||||
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-i18next": "^16.3.5",
|
||||||
"react-router-dom": "^6.28.0"
|
"react-router-dom": "^6.28.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
56
frontend/src/components/LanguageSelector.tsx
Normal file
56
frontend/src/components/LanguageSelector.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { LANGUAGES, type LanguageCode } from '@/i18n'
|
||||||
|
|
||||||
|
interface LanguageSelectorProps {
|
||||||
|
/** Additional CSS classes */
|
||||||
|
className?: string
|
||||||
|
/** Compact mode - shows only flag and code */
|
||||||
|
compact?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dropdown component for selecting the application language.
|
||||||
|
* Uses i18next for language switching and persists choice to localStorage.
|
||||||
|
*/
|
||||||
|
export default function LanguageSelector({ className = '', compact = false }: LanguageSelectorProps) {
|
||||||
|
const { i18n, t } = useTranslation('common')
|
||||||
|
|
||||||
|
const handleLanguageChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const newLang = e.target.value as LanguageCode
|
||||||
|
i18n.changeLanguage(newLang)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`relative ${className}`}>
|
||||||
|
<select
|
||||||
|
value={i18n.language}
|
||||||
|
onChange={handleLanguageChange}
|
||||||
|
className="appearance-none bg-white border border-gray-300 rounded-md pl-3 pr-8 py-1.5 text-sm text-gray-700 hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent cursor-pointer"
|
||||||
|
title={t('selectLanguage')}
|
||||||
|
aria-label={t('selectLanguage')}
|
||||||
|
>
|
||||||
|
{LANGUAGES.map((lang) => (
|
||||||
|
<option key={lang.code} value={lang.code}>
|
||||||
|
{compact ? `${lang.flag} ${lang.code.toUpperCase()}` : `${lang.flag} ${lang.label}`}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{/* Custom dropdown arrow */}
|
||||||
|
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
|
<svg
|
||||||
|
className="h-4 w-4 text-gray-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M19 9l-7 7-7-7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import Navbar from './Navbar'
|
import Navbar from './Navbar'
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
@@ -6,6 +7,8 @@ interface LayoutProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Layout({ children }: LayoutProps) {
|
export default function Layout({ children }: LayoutProps) {
|
||||||
|
const { t } = useTranslation('navbar')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col">
|
<div className="flex min-h-screen flex-col">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
@@ -17,8 +20,7 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
<footer className="border-t border-gray-200 bg-white py-6">
|
<footer className="border-t border-gray-200 bg-white py-6">
|
||||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
<p className="text-center text-sm text-gray-500">
|
<p className="text-center text-sm text-gray-500">
|
||||||
© {new Date().getFullYear()} Requirements Periodic Table. All rights
|
© {new Date().getFullYear()} {t('brand')}.
|
||||||
reserved.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useAuth } from '@/hooks'
|
import { useAuth } from '@/hooks'
|
||||||
|
import LanguageSelector from './LanguageSelector'
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
const { user, isAuthenticated, login, logout } = useAuth()
|
const { user, isAuthenticated, login, logout } = useAuth()
|
||||||
|
const { t } = useTranslation('navbar')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="border-b border-gray-200 bg-white shadow-sm">
|
<nav className="border-b border-gray-200 bg-white shadow-sm">
|
||||||
@@ -27,23 +30,26 @@ export default function Navbar() {
|
|||||||
<rect x="9" y="3" width="6" height="4" rx="1" />
|
<rect x="9" y="3" width="6" height="4" rx="1" />
|
||||||
<path d="M9 12l2 2 4-4" />
|
<path d="M9 12l2 2 4-4" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Requirements Periodic Table</span>
|
<span>{t('brand')}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation Links */}
|
{/* Navigation Links */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
|
{/* Language Selector */}
|
||||||
|
<LanguageSelector compact />
|
||||||
|
|
||||||
{isAuthenticated ? (
|
{isAuthenticated ? (
|
||||||
<>
|
<>
|
||||||
<Link
|
<Link
|
||||||
to="/dashboard"
|
to="/dashboard"
|
||||||
className="rounded-md px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-900"
|
className="rounded-md px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-900"
|
||||||
>
|
>
|
||||||
Dashboard
|
{t('dashboard')}
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-sm text-gray-600">
|
<span className="text-sm text-gray-600">
|
||||||
Hello,{' '}
|
{t('hello')},{' '}
|
||||||
<span className="font-medium text-gray-900">
|
<span className="font-medium text-gray-900">
|
||||||
{user?.full_name || user?.preferred_username}
|
{user?.full_name || user?.preferred_username}
|
||||||
</span>
|
</span>
|
||||||
@@ -52,7 +58,7 @@ export default function Navbar() {
|
|||||||
onClick={logout}
|
onClick={logout}
|
||||||
className="rounded-md bg-gray-100 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-200"
|
className="rounded-md bg-gray-100 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-200"
|
||||||
>
|
>
|
||||||
Logout
|
{t('logout')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -61,7 +67,7 @@ export default function Navbar() {
|
|||||||
onClick={login}
|
onClick={login}
|
||||||
className="rounded-md bg-primary-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-700"
|
className="rounded-md bg-primary-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-700"
|
||||||
>
|
>
|
||||||
Login
|
{t('login')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export { default as Layout } from './Layout'
|
export { default as Layout } from './Layout'
|
||||||
export { default as Navbar } from './Navbar'
|
export { default as Navbar } from './Navbar'
|
||||||
export { default as ProtectedRoute } from './ProtectedRoute'
|
export { default as ProtectedRoute } from './ProtectedRoute'
|
||||||
|
export { default as LanguageSelector } from './LanguageSelector'
|
||||||
|
|||||||
9
frontend/src/i18n/i18n.d.ts
vendored
Normal file
9
frontend/src/i18n/i18n.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import 'i18next';
|
||||||
|
import { resources, defaultNS } from './index';
|
||||||
|
|
||||||
|
declare module 'i18next' {
|
||||||
|
interface CustomTypeOptions {
|
||||||
|
defaultNS: typeof defaultNS;
|
||||||
|
resources: typeof resources['en'];
|
||||||
|
}
|
||||||
|
}
|
||||||
96
frontend/src/i18n/index.ts
Normal file
96
frontend/src/i18n/index.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import i18n from 'i18next';
|
||||||
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||||
|
|
||||||
|
// Import English translations
|
||||||
|
import enCommon from './locales/en/common.json';
|
||||||
|
import enNavbar from './locales/en/navbar.json';
|
||||||
|
import enHome from './locales/en/home.json';
|
||||||
|
import enDashboard from './locales/en/dashboard.json';
|
||||||
|
import enRequirements from './locales/en/requirements.json';
|
||||||
|
import enRequirementDetail from './locales/en/requirementDetail.json';
|
||||||
|
import enAdmin from './locales/en/admin.json';
|
||||||
|
import enValidation from './locales/en/validation.json';
|
||||||
|
import enErrors from './locales/en/errors.json';
|
||||||
|
|
||||||
|
// Import Portuguese translations
|
||||||
|
import ptCommon from './locales/pt/common.json';
|
||||||
|
import ptNavbar from './locales/pt/navbar.json';
|
||||||
|
import ptHome from './locales/pt/home.json';
|
||||||
|
import ptDashboard from './locales/pt/dashboard.json';
|
||||||
|
import ptRequirements from './locales/pt/requirements.json';
|
||||||
|
import ptRequirementDetail from './locales/pt/requirementDetail.json';
|
||||||
|
import ptAdmin from './locales/pt/admin.json';
|
||||||
|
import ptValidation from './locales/pt/validation.json';
|
||||||
|
import ptErrors from './locales/pt/errors.json';
|
||||||
|
|
||||||
|
// Define available languages for the language selector
|
||||||
|
export const LANGUAGES = [
|
||||||
|
{ code: 'en', label: 'English', flag: '🇺🇸' },
|
||||||
|
{ code: 'pt', label: 'Português', flag: '🇧🇷' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type LanguageCode = typeof LANGUAGES[number]['code'];
|
||||||
|
|
||||||
|
// Define resources with all namespaces
|
||||||
|
export const resources = {
|
||||||
|
en: {
|
||||||
|
common: enCommon,
|
||||||
|
navbar: enNavbar,
|
||||||
|
home: enHome,
|
||||||
|
dashboard: enDashboard,
|
||||||
|
requirements: enRequirements,
|
||||||
|
requirementDetail: enRequirementDetail,
|
||||||
|
admin: enAdmin,
|
||||||
|
validation: enValidation,
|
||||||
|
errors: enErrors,
|
||||||
|
},
|
||||||
|
pt: {
|
||||||
|
common: ptCommon,
|
||||||
|
navbar: ptNavbar,
|
||||||
|
home: ptHome,
|
||||||
|
dashboard: ptDashboard,
|
||||||
|
requirements: ptRequirements,
|
||||||
|
requirementDetail: ptRequirementDetail,
|
||||||
|
admin: ptAdmin,
|
||||||
|
validation: ptValidation,
|
||||||
|
errors: ptErrors,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Define default namespace
|
||||||
|
export const defaultNS = 'common';
|
||||||
|
|
||||||
|
i18n
|
||||||
|
// Detect user language from localStorage, navigator, etc.
|
||||||
|
.use(LanguageDetector)
|
||||||
|
// Pass i18n instance to react-i18next
|
||||||
|
.use(initReactI18next)
|
||||||
|
// Initialize i18next
|
||||||
|
.init({
|
||||||
|
resources,
|
||||||
|
fallbackLng: 'en',
|
||||||
|
defaultNS,
|
||||||
|
|
||||||
|
// Language detection options
|
||||||
|
detection: {
|
||||||
|
// Order of language detection methods
|
||||||
|
order: ['localStorage', 'navigator', 'htmlTag'],
|
||||||
|
// Cache user language preference in localStorage
|
||||||
|
caches: ['localStorage'],
|
||||||
|
// Key name in localStorage
|
||||||
|
lookupLocalStorage: 'i18nextLng',
|
||||||
|
},
|
||||||
|
|
||||||
|
interpolation: {
|
||||||
|
// React already escapes values, so we don't need i18next to do it
|
||||||
|
escapeValue: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// React Suspense support (for future lazy loading)
|
||||||
|
react: {
|
||||||
|
useSuspense: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
68
frontend/src/i18n/locales/en/admin.json
Normal file
68
frontend/src/i18n/locales/en/admin.json
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"pageTitle": "Admin Panel",
|
||||||
|
"managing": "Managing:",
|
||||||
|
"tabs": {
|
||||||
|
"projectSettings": "Project Settings",
|
||||||
|
"memberRoles": "Member Roles",
|
||||||
|
"relationshipTypes": "Relationship Types"
|
||||||
|
},
|
||||||
|
"backToDashboard": "Back to Dashboard",
|
||||||
|
"noProject": {
|
||||||
|
"title": "No Project Selected",
|
||||||
|
"message": "Please select a project from the dashboard first.",
|
||||||
|
"goToDashboard": "Go to Dashboard"
|
||||||
|
},
|
||||||
|
"projectSettings": {
|
||||||
|
"title": "Project Settings",
|
||||||
|
"projectName": "Project Name",
|
||||||
|
"description": "Description",
|
||||||
|
"saveButton": "Save Changes",
|
||||||
|
"successMessage": "Project updated successfully!",
|
||||||
|
"errorUpdating": "Failed to update project"
|
||||||
|
},
|
||||||
|
"memberRoles": {
|
||||||
|
"title": "Member Roles",
|
||||||
|
"loadingMembers": "Loading members...",
|
||||||
|
"noMembers": "No members found",
|
||||||
|
"tableHeaders": {
|
||||||
|
"user": "User",
|
||||||
|
"role": "Role",
|
||||||
|
"actions": "Actions"
|
||||||
|
},
|
||||||
|
"youBadge": "You",
|
||||||
|
"cannotDemoteSelf": "You cannot demote yourself. Ask another admin to change your role.",
|
||||||
|
"errorUpdating": "Failed to update member role"
|
||||||
|
},
|
||||||
|
"relationshipTypes": {
|
||||||
|
"title": "Relationship Types",
|
||||||
|
"addButton": "+ Add Type",
|
||||||
|
"loadingTypes": "Loading relationship types...",
|
||||||
|
"noTypes": "No relationship types defined yet. Create one to link requirements.",
|
||||||
|
"inverse": "Inverse:",
|
||||||
|
"editButton": "Edit",
|
||||||
|
"deleteButton": "Delete"
|
||||||
|
},
|
||||||
|
"createRelTypeModal": {
|
||||||
|
"title": "Create Relationship Type",
|
||||||
|
"typeName": "Type Name",
|
||||||
|
"typeNamePlaceholder": "e.g., Depends On",
|
||||||
|
"inverseName": "Inverse Name",
|
||||||
|
"inverseNamePlaceholder": "e.g., Depended By",
|
||||||
|
"inverseNameHint": "Optional. The name shown when viewing from the target requirement.",
|
||||||
|
"description": "Description",
|
||||||
|
"createButton": "Create",
|
||||||
|
"errorCreating": "Failed to create relationship type"
|
||||||
|
},
|
||||||
|
"editRelTypeModal": {
|
||||||
|
"title": "Edit Relationship Type",
|
||||||
|
"saveButton": "Save Changes",
|
||||||
|
"errorUpdating": "Failed to update relationship type"
|
||||||
|
},
|
||||||
|
"deleteRelTypeModal": {
|
||||||
|
"title": "Delete Relationship Type",
|
||||||
|
"confirmMessage": "Are you sure you want to delete the relationship type",
|
||||||
|
"warningMessage": "⚠️ This will also delete all requirement links using this type.",
|
||||||
|
"deleteButton": "Delete",
|
||||||
|
"errorDeleting": "Failed to delete relationship type"
|
||||||
|
}
|
||||||
|
}
|
||||||
49
frontend/src/i18n/locales/en/common.json
Normal file
49
frontend/src/i18n/locales/en/common.json
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"save": "Save",
|
||||||
|
"saveChanges": "Save Changes",
|
||||||
|
"saving": "Saving...",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"delete": "Delete",
|
||||||
|
"deleting": "Deleting...",
|
||||||
|
"edit": "Edit",
|
||||||
|
"close": "Close",
|
||||||
|
"loading": "Loading...",
|
||||||
|
"retry": "Retry",
|
||||||
|
"search": "Search",
|
||||||
|
"clear": "Clear",
|
||||||
|
"filter": "Filter",
|
||||||
|
"create": "Create",
|
||||||
|
"creating": "Creating...",
|
||||||
|
"add": "Add",
|
||||||
|
"remove": "Remove",
|
||||||
|
"yes": "Yes",
|
||||||
|
"no": "No",
|
||||||
|
"none": "None",
|
||||||
|
"unknown": "Unknown",
|
||||||
|
"noResults": "No results found",
|
||||||
|
"required": "Required",
|
||||||
|
"optional": "Optional",
|
||||||
|
"version": "Version",
|
||||||
|
"priority": "Priority",
|
||||||
|
"status": "Status",
|
||||||
|
"date": "Date",
|
||||||
|
"name": "Name",
|
||||||
|
"description": "Description",
|
||||||
|
"actions": "Actions",
|
||||||
|
"details": "Details",
|
||||||
|
"user": "User",
|
||||||
|
"role": "Role",
|
||||||
|
"by": "by",
|
||||||
|
"on": "on",
|
||||||
|
"more": "more",
|
||||||
|
"empty": "Empty",
|
||||||
|
"old": "Old",
|
||||||
|
"new": "New",
|
||||||
|
"total": "Total",
|
||||||
|
"all": "All",
|
||||||
|
"you": "You",
|
||||||
|
"projects": "Projects",
|
||||||
|
"project": "Project",
|
||||||
|
"selectLanguage": "Select language",
|
||||||
|
"logout": "Logout"
|
||||||
|
}
|
||||||
67
frontend/src/i18n/locales/en/dashboard.json
Normal file
67
frontend/src/i18n/locales/en/dashboard.json
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"sidebar": {
|
||||||
|
"title": "Digital Twin",
|
||||||
|
"subtitle": "Requirements Tool",
|
||||||
|
"createRequirement": "Create Requirement",
|
||||||
|
"navigation": "Navigation",
|
||||||
|
"searchRequirements": "Search Requirements",
|
||||||
|
"myRequirements": "My Requirements",
|
||||||
|
"generateReport": "Generate Report",
|
||||||
|
"projectSummary": "Project Summary",
|
||||||
|
"total": "Total",
|
||||||
|
"validated": "Validated"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"projects": "Projects",
|
||||||
|
"admin": "Admin"
|
||||||
|
},
|
||||||
|
"projectDropdown": {
|
||||||
|
"loading": "Loading...",
|
||||||
|
"noProjectSelected": "No project selected",
|
||||||
|
"noProjectsAvailable": "No projects available",
|
||||||
|
"createNewProject": "+ Create New Project"
|
||||||
|
},
|
||||||
|
"noProjectWarning": {
|
||||||
|
"title": "No Project Selected",
|
||||||
|
"messagePart1": "Please select a project from the dropdown above or",
|
||||||
|
"createProject": "create a new project",
|
||||||
|
"messagePart2": "to get started."
|
||||||
|
},
|
||||||
|
"quickFilters": {
|
||||||
|
"title": "Quick Search Filters",
|
||||||
|
"subtitle": "Click a category to filter requirements",
|
||||||
|
"noGroupsFound": "No groups found",
|
||||||
|
"total": "total"
|
||||||
|
},
|
||||||
|
"needsAttention": {
|
||||||
|
"title": "Needs Attention",
|
||||||
|
"subtitle": "Requirements with denied or partial validation",
|
||||||
|
"viewAll": "View All",
|
||||||
|
"moreRequirements": "more requirements need attention",
|
||||||
|
"by": "by"
|
||||||
|
},
|
||||||
|
"needsRevalidation": {
|
||||||
|
"title": "Needs Revalidation",
|
||||||
|
"subtitle": "Requirements updated since last validation",
|
||||||
|
"viewAll": "View All",
|
||||||
|
"moreRequirements": "more requirements need revalidation",
|
||||||
|
"versionBehind": "version behind",
|
||||||
|
"versionsBehind": "versions behind",
|
||||||
|
"lastValidatedBy": "Last validated by",
|
||||||
|
"by": "by"
|
||||||
|
},
|
||||||
|
"allClear": {
|
||||||
|
"title": "All Clear!",
|
||||||
|
"message": "No requirements need attention. All validations are either approved or pending review."
|
||||||
|
},
|
||||||
|
"createProject": {
|
||||||
|
"title": "Create New Project",
|
||||||
|
"projectName": "Project Name",
|
||||||
|
"projectNamePlaceholder": "Enter project name",
|
||||||
|
"description": "Description",
|
||||||
|
"descriptionPlaceholder": "Enter project description (optional)",
|
||||||
|
"createButton": "Create Project",
|
||||||
|
"creating": "Creating...",
|
||||||
|
"errorCreating": "Failed to create project. Please try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
11
frontend/src/i18n/locales/en/errors.json
Normal file
11
frontend/src/i18n/locales/en/errors.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"generic": "An error occurred. Please try again.",
|
||||||
|
"loadFailed": "Failed to load data. Please try again.",
|
||||||
|
"saveFailed": "Failed to save. Please try again.",
|
||||||
|
"deleteFailed": "Failed to delete. Please try again.",
|
||||||
|
"createFailed": "Failed to create. Please try again.",
|
||||||
|
"unauthorized": "You are not authorized to perform this action.",
|
||||||
|
"notFound": "The requested resource was not found.",
|
||||||
|
"networkError": "Network error. Please check your connection.",
|
||||||
|
"sessionExpired": "Your session has expired. Please log in again."
|
||||||
|
}
|
||||||
5
frontend/src/i18n/locales/en/home.json
Normal file
5
frontend/src/i18n/locales/en/home.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"title": "Requirements Periodic Table",
|
||||||
|
"subtitle": "Manage and track your project requirements.",
|
||||||
|
"goToDashboard": "Go to Dashboard"
|
||||||
|
}
|
||||||
7
frontend/src/i18n/locales/en/navbar.json
Normal file
7
frontend/src/i18n/locales/en/navbar.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"brand": "Requirements Periodic Table",
|
||||||
|
"dashboard": "Dashboard",
|
||||||
|
"hello": "Hello",
|
||||||
|
"logout": "Logout",
|
||||||
|
"login": "Login"
|
||||||
|
}
|
||||||
155
frontend/src/i18n/locales/en/requirementDetail.json
Normal file
155
frontend/src/i18n/locales/en/requirementDetail.json
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
{
|
||||||
|
"pageTitle": "Digital Twin Requirements Tool",
|
||||||
|
"breadcrumb": {
|
||||||
|
"search": "Search",
|
||||||
|
"details": "Details"
|
||||||
|
},
|
||||||
|
"loadingRequirement": "Loading requirement...",
|
||||||
|
"requirementNotFound": "Requirement not found",
|
||||||
|
"noRequirementId": "No requirement ID provided",
|
||||||
|
"backToRequirements": "Back to Requirements",
|
||||||
|
"errorLoading": "Failed to load requirement. Please try again.",
|
||||||
|
"draft": {
|
||||||
|
"badge": "Draft - Not Finalized",
|
||||||
|
"title": "Draft Requirement",
|
||||||
|
"message": "This requirement is still in draft status and is not finalized. It may be subject to changes."
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"description": "Description",
|
||||||
|
"relationships": "Relationships",
|
||||||
|
"acceptanceCriteria": "Acceptance Criteria",
|
||||||
|
"sharedComments": "Shared Comments",
|
||||||
|
"validate": "Validate",
|
||||||
|
"history": "History"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"title": "Description",
|
||||||
|
"version": "Version:",
|
||||||
|
"status": "Status:",
|
||||||
|
"validationStatus": "Validation Status:",
|
||||||
|
"author": "Author:",
|
||||||
|
"lastEditedBy": "Last Edited By:",
|
||||||
|
"created": "Created:",
|
||||||
|
"lastUpdated": "Last Updated:",
|
||||||
|
"noDescription": "No description provided.",
|
||||||
|
"editButton": "Edit"
|
||||||
|
},
|
||||||
|
"relationships": {
|
||||||
|
"title": "Relationships",
|
||||||
|
"addButton": "Add Relationship",
|
||||||
|
"noTypesWarning": "No relationship types have been defined for this project. Contact an administrator to set up relationship types.",
|
||||||
|
"loadingRelationships": "Loading relationships...",
|
||||||
|
"noRelationships": "No relationships defined yet.",
|
||||||
|
"tableHeaders": {
|
||||||
|
"direction": "Direction",
|
||||||
|
"type": "Type",
|
||||||
|
"linkedRequirement": "Linked Requirement",
|
||||||
|
"createdBy": "Created By",
|
||||||
|
"date": "Date",
|
||||||
|
"actions": "Actions"
|
||||||
|
},
|
||||||
|
"outgoing": "→ Outgoing",
|
||||||
|
"incoming": "← Incoming"
|
||||||
|
},
|
||||||
|
"acceptanceCriteria": {
|
||||||
|
"title": "Acceptance Criteria",
|
||||||
|
"noCriteria": "No acceptance criteria defined yet.",
|
||||||
|
"addButton": "Add Criterion"
|
||||||
|
},
|
||||||
|
"comments": {
|
||||||
|
"title": "Shared Comments",
|
||||||
|
"placeholder": "Add a comment...",
|
||||||
|
"postButton": "Post Comment",
|
||||||
|
"posting": "Posting...",
|
||||||
|
"noComments": "No comments yet. Be the first to comment!",
|
||||||
|
"loadingComments": "Loading comments...",
|
||||||
|
"reply": "Reply",
|
||||||
|
"cancelReply": "Cancel Reply",
|
||||||
|
"replyPlaceholder": "Write a reply...",
|
||||||
|
"postReply": "Post Reply",
|
||||||
|
"errorPosting": "Failed to post comment. Please try again.",
|
||||||
|
"errorPostingReply": "Failed to post reply. Please try again.",
|
||||||
|
"roles": {
|
||||||
|
"editor": "Editor",
|
||||||
|
"auditor": "Auditor",
|
||||||
|
"admin": "Project Admin",
|
||||||
|
"user": "User"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"validate": {
|
||||||
|
"title": "Validate Requirement",
|
||||||
|
"currentStatus": "Current Status:",
|
||||||
|
"requirementVersion": "Requirement Version:",
|
||||||
|
"lastValidatedBy": "Last validated by",
|
||||||
|
"staleWarning": "This requirement was modified after the last validation (validated at version {{validationVersion}}, current version {{currentVersion}}).",
|
||||||
|
"submitTitle": "Submit Validation",
|
||||||
|
"statusLabel": "Validation Status",
|
||||||
|
"selectStatus": "Select a status...",
|
||||||
|
"commentLabel": "Comment",
|
||||||
|
"commentPlaceholder": "Add a comment explaining your decision (optional but recommended)",
|
||||||
|
"submitButton": "Submit Validation",
|
||||||
|
"submitting": "Submitting...",
|
||||||
|
"errorSubmitting": "Failed to submit validation. Please try again.",
|
||||||
|
"historyTitle": "Validation History",
|
||||||
|
"loadingHistory": "Loading history...",
|
||||||
|
"noHistory": "No validation history yet.",
|
||||||
|
"tableHeaders": {
|
||||||
|
"date": "Date",
|
||||||
|
"status": "Status",
|
||||||
|
"version": "Version",
|
||||||
|
"validator": "Validator",
|
||||||
|
"comment": "Comment"
|
||||||
|
},
|
||||||
|
"noComment": "No comment"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"title": "Version History",
|
||||||
|
"originalAuthor": "Original Author:",
|
||||||
|
"loadingHistory": "Loading history...",
|
||||||
|
"noHistory": "No history yet. History is recorded when the requirement is edited or relationships change.",
|
||||||
|
"requirementEdited": "Requirement edited",
|
||||||
|
"linkCreated": "Link Created",
|
||||||
|
"linkRemoved": "Link Removed",
|
||||||
|
"groupAdded": "Group Added",
|
||||||
|
"groupRemoved": "Group Removed",
|
||||||
|
"noChangesDetected": "No visible changes detected (may be a group-only change).",
|
||||||
|
"showLess": "Show less",
|
||||||
|
"showFullDiff": "Show full diff",
|
||||||
|
"deletedRequirement": "Deleted Requirement",
|
||||||
|
"unknownGroup": "Unknown Group"
|
||||||
|
},
|
||||||
|
"addRelationshipModal": {
|
||||||
|
"title": "Add Relationship",
|
||||||
|
"relationshipType": "Relationship Type",
|
||||||
|
"selectType": "Select a relationship type...",
|
||||||
|
"targetRequirement": "Target Requirement",
|
||||||
|
"searchPlaceholder": "Search by tag code or name...",
|
||||||
|
"selected": "Selected:",
|
||||||
|
"createButton": "Create Relationship",
|
||||||
|
"errorCreating": "Failed to create link. Please try again."
|
||||||
|
},
|
||||||
|
"editModal": {
|
||||||
|
"title": "Edit Requirement",
|
||||||
|
"loadingOptions": "Loading options...",
|
||||||
|
"name": "Name",
|
||||||
|
"tag": "Tag",
|
||||||
|
"selectTag": "Select a tag...",
|
||||||
|
"priority": "Priority",
|
||||||
|
"noPriority": "No priority",
|
||||||
|
"status": "Status",
|
||||||
|
"draftNote": "Draft requirements are not finalized and marked with a visual indicator.",
|
||||||
|
"description": "Description",
|
||||||
|
"groups": "Groups",
|
||||||
|
"noGroupsAvailable": "No groups available",
|
||||||
|
"errorSaving": "Failed to save changes. Please try again.",
|
||||||
|
"nameTagRequired": "Name and Tag are required"
|
||||||
|
},
|
||||||
|
"deleteModal": {
|
||||||
|
"deleteComment": "Delete Comment",
|
||||||
|
"deleteReply": "Delete Reply",
|
||||||
|
"deleteRelationship": "Delete Relationship",
|
||||||
|
"confirmDeleteComment": "Are you sure you want to delete this comment? This will also hide all replies.",
|
||||||
|
"confirmDeleteReply": "Are you sure you want to delete this reply?",
|
||||||
|
"confirmDeleteRelationship": "Are you sure you want to delete this relationship? This action cannot be undone."
|
||||||
|
}
|
||||||
|
}
|
||||||
87
frontend/src/i18n/locales/en/requirements.json
Normal file
87
frontend/src/i18n/locales/en/requirements.json
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"title": "Digital Twin Requirements Tool"
|
||||||
|
},
|
||||||
|
"breadcrumb": {
|
||||||
|
"projects": "Projects",
|
||||||
|
"searchRequirements": "Search Requirements"
|
||||||
|
},
|
||||||
|
"noProjectSelected": {
|
||||||
|
"title": "No Project Selected",
|
||||||
|
"message": "Please select a project from the dashboard to view requirements.",
|
||||||
|
"goToDashboard": "Go to Dashboard"
|
||||||
|
},
|
||||||
|
"loadingRequirements": "Loading requirements...",
|
||||||
|
"newRequirement": "New Requirement",
|
||||||
|
"deleted": "Deleted",
|
||||||
|
"searchPlaceholder": "Search for a requirement tag or title",
|
||||||
|
"noRequirementsFound": "No requirements found matching your criteria.",
|
||||||
|
"filters": {
|
||||||
|
"filterGroup": "Filter Group",
|
||||||
|
"filterValidationStatus": "Filter Validation Status",
|
||||||
|
"showingNeedsRevalidation": "Showing: Needs Revalidation",
|
||||||
|
"showingNeedsAttention": "Showing: Needs Attention",
|
||||||
|
"needsRevalidation": "Needs Revalidation",
|
||||||
|
"statuses": {
|
||||||
|
"approved": "Approved",
|
||||||
|
"denied": "Denied",
|
||||||
|
"partiallyApproved": "Partially Approved",
|
||||||
|
"notValidated": "Not Validated"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"orderBy": {
|
||||||
|
"label": "Order by",
|
||||||
|
"date": "Date",
|
||||||
|
"priority": "Priority",
|
||||||
|
"name": "Name"
|
||||||
|
},
|
||||||
|
"caption": {
|
||||||
|
"title": "Tag Caption",
|
||||||
|
"viewCaptions": "View tag captions"
|
||||||
|
},
|
||||||
|
"requirement": {
|
||||||
|
"draft": "Draft",
|
||||||
|
"draftTooltip": "This requirement is still in draft and not finalized",
|
||||||
|
"noGroups": "No groups",
|
||||||
|
"more": "more",
|
||||||
|
"stale": "Stale",
|
||||||
|
"staleTooltip": "Requirement was modified after validation",
|
||||||
|
"by": "by",
|
||||||
|
"priority": "Priority",
|
||||||
|
"version": "Version"
|
||||||
|
},
|
||||||
|
"deletedPanel": {
|
||||||
|
"title": "Deleted Requirements",
|
||||||
|
"noDeleted": "No deleted requirements found.",
|
||||||
|
"deletedWillAppear": "Deleted requirements will appear here.",
|
||||||
|
"unnamed": "Unnamed Requirement",
|
||||||
|
"originalId": "Original ID",
|
||||||
|
"deletedAt": "Deleted",
|
||||||
|
"deletedBy": "Deleted by",
|
||||||
|
"footerNote": "Deleted requirements are preserved in history for auditing purposes."
|
||||||
|
},
|
||||||
|
"deleteModal": {
|
||||||
|
"title": "Delete Requirement",
|
||||||
|
"confirmMessage": "Are you sure you want to delete the requirement",
|
||||||
|
"explanation": "This action will move the requirement to the deleted items. You can view deleted requirements from the \"Deleted\" panel.",
|
||||||
|
"deleting": "Deleting..."
|
||||||
|
},
|
||||||
|
"createModal": {
|
||||||
|
"title": "New Requirement",
|
||||||
|
"tag": "Tag",
|
||||||
|
"selectTag": "Select a tag...",
|
||||||
|
"name": "Name",
|
||||||
|
"namePlaceholder": "Enter requirement name",
|
||||||
|
"description": "Description",
|
||||||
|
"descriptionPlaceholder": "Enter requirement description (optional)",
|
||||||
|
"priority": "Priority",
|
||||||
|
"selectPriority": "Select a priority (optional)...",
|
||||||
|
"groups": "Groups",
|
||||||
|
"createButton": "Create Requirement",
|
||||||
|
"creating": "Creating...",
|
||||||
|
"errorCreating": "Failed to create requirement. Please try again.",
|
||||||
|
"noProjectSelected": "No project selected",
|
||||||
|
"nameRequired": "Requirement name is required",
|
||||||
|
"tagRequired": "Please select a tag"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
frontend/src/i18n/locales/en/validation.json
Normal file
7
frontend/src/i18n/locales/en/validation.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"approved": "Approved",
|
||||||
|
"denied": "Denied",
|
||||||
|
"partial": "Partial",
|
||||||
|
"partiallyApproved": "Partially Approved",
|
||||||
|
"notValidated": "Not Validated"
|
||||||
|
}
|
||||||
68
frontend/src/i18n/locales/pt/admin.json
Normal file
68
frontend/src/i18n/locales/pt/admin.json
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"pageTitle": "Painel de Administração",
|
||||||
|
"managing": "Gerenciando:",
|
||||||
|
"tabs": {
|
||||||
|
"projectSettings": "Configurações do Projeto",
|
||||||
|
"memberRoles": "Funções dos Membros",
|
||||||
|
"relationshipTypes": "Tipos de Relacionamento"
|
||||||
|
},
|
||||||
|
"backToDashboard": "Voltar ao Painel",
|
||||||
|
"noProject": {
|
||||||
|
"title": "Nenhum Projeto Selecionado",
|
||||||
|
"message": "Por favor, selecione um projeto no painel primeiro.",
|
||||||
|
"goToDashboard": "Ir para o Painel"
|
||||||
|
},
|
||||||
|
"projectSettings": {
|
||||||
|
"title": "Configurações do Projeto",
|
||||||
|
"projectName": "Nome do Projeto",
|
||||||
|
"description": "Descrição",
|
||||||
|
"saveButton": "Salvar Alterações",
|
||||||
|
"successMessage": "Projeto atualizado com sucesso!",
|
||||||
|
"errorUpdating": "Falha ao atualizar projeto"
|
||||||
|
},
|
||||||
|
"memberRoles": {
|
||||||
|
"title": "Funções dos Membros",
|
||||||
|
"loadingMembers": "Carregando membros...",
|
||||||
|
"noMembers": "Nenhum membro encontrado",
|
||||||
|
"tableHeaders": {
|
||||||
|
"user": "Usuário",
|
||||||
|
"role": "Função",
|
||||||
|
"actions": "Ações"
|
||||||
|
},
|
||||||
|
"youBadge": "Você",
|
||||||
|
"cannotDemoteSelf": "Você não pode rebaixar a si mesmo. Peça a outro administrador para alterar sua função.",
|
||||||
|
"errorUpdating": "Falha ao atualizar função do membro"
|
||||||
|
},
|
||||||
|
"relationshipTypes": {
|
||||||
|
"title": "Tipos de Relacionamento",
|
||||||
|
"addButton": "+ Adicionar Tipo",
|
||||||
|
"loadingTypes": "Carregando tipos de relacionamento...",
|
||||||
|
"noTypes": "Nenhum tipo de relacionamento definido ainda. Crie um para vincular requisitos.",
|
||||||
|
"inverse": "Inverso:",
|
||||||
|
"editButton": "Editar",
|
||||||
|
"deleteButton": "Excluir"
|
||||||
|
},
|
||||||
|
"createRelTypeModal": {
|
||||||
|
"title": "Criar Tipo de Relacionamento",
|
||||||
|
"typeName": "Nome do Tipo",
|
||||||
|
"typeNamePlaceholder": "ex.: Depende De",
|
||||||
|
"inverseName": "Nome Inverso",
|
||||||
|
"inverseNamePlaceholder": "ex.: É Dependência De",
|
||||||
|
"inverseNameHint": "Opcional. O nome mostrado ao visualizar do requisito de destino.",
|
||||||
|
"description": "Descrição",
|
||||||
|
"createButton": "Criar",
|
||||||
|
"errorCreating": "Falha ao criar tipo de relacionamento"
|
||||||
|
},
|
||||||
|
"editRelTypeModal": {
|
||||||
|
"title": "Editar Tipo de Relacionamento",
|
||||||
|
"saveButton": "Salvar Alterações",
|
||||||
|
"errorUpdating": "Falha ao atualizar tipo de relacionamento"
|
||||||
|
},
|
||||||
|
"deleteRelTypeModal": {
|
||||||
|
"title": "Excluir Tipo de Relacionamento",
|
||||||
|
"confirmMessage": "Tem certeza de que deseja excluir o tipo de relacionamento",
|
||||||
|
"warningMessage": "⚠️ Isso também excluirá todos os links de requisitos usando este tipo.",
|
||||||
|
"deleteButton": "Excluir",
|
||||||
|
"errorDeleting": "Falha ao excluir tipo de relacionamento"
|
||||||
|
}
|
||||||
|
}
|
||||||
49
frontend/src/i18n/locales/pt/common.json
Normal file
49
frontend/src/i18n/locales/pt/common.json
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"save": "Salvar",
|
||||||
|
"saveChanges": "Salvar Alterações",
|
||||||
|
"saving": "Salvando...",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"delete": "Excluir",
|
||||||
|
"deleting": "Excluindo...",
|
||||||
|
"edit": "Editar",
|
||||||
|
"close": "Fechar",
|
||||||
|
"loading": "Carregando...",
|
||||||
|
"retry": "Tentar Novamente",
|
||||||
|
"search": "Buscar",
|
||||||
|
"clear": "Limpar",
|
||||||
|
"filter": "Filtrar",
|
||||||
|
"create": "Criar",
|
||||||
|
"creating": "Criando...",
|
||||||
|
"add": "Adicionar",
|
||||||
|
"remove": "Remover",
|
||||||
|
"yes": "Sim",
|
||||||
|
"no": "Não",
|
||||||
|
"none": "Nenhum",
|
||||||
|
"unknown": "Desconhecido",
|
||||||
|
"noResults": "Nenhum resultado encontrado",
|
||||||
|
"required": "Obrigatório",
|
||||||
|
"optional": "Opcional",
|
||||||
|
"version": "Versão",
|
||||||
|
"priority": "Prioridade",
|
||||||
|
"status": "Status",
|
||||||
|
"date": "Data",
|
||||||
|
"name": "Nome",
|
||||||
|
"description": "Descrição",
|
||||||
|
"actions": "Ações",
|
||||||
|
"details": "Detalhes",
|
||||||
|
"user": "Usuário",
|
||||||
|
"role": "Função",
|
||||||
|
"by": "por",
|
||||||
|
"on": "em",
|
||||||
|
"more": "mais",
|
||||||
|
"empty": "Vazio",
|
||||||
|
"old": "Antigo",
|
||||||
|
"new": "Novo",
|
||||||
|
"total": "Total",
|
||||||
|
"all": "Todos",
|
||||||
|
"you": "Você",
|
||||||
|
"projects": "Projetos",
|
||||||
|
"project": "Projeto",
|
||||||
|
"selectLanguage": "Selecionar idioma",
|
||||||
|
"logout": "Sair"
|
||||||
|
}
|
||||||
67
frontend/src/i18n/locales/pt/dashboard.json
Normal file
67
frontend/src/i18n/locales/pt/dashboard.json
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"sidebar": {
|
||||||
|
"title": "Digital Twin",
|
||||||
|
"subtitle": "Ferramenta de Requisitos",
|
||||||
|
"createRequirement": "Criar Requisito",
|
||||||
|
"navigation": "Navegação",
|
||||||
|
"searchRequirements": "Buscar Requisitos",
|
||||||
|
"myRequirements": "Meus Requisitos",
|
||||||
|
"generateReport": "Gerar Relatório",
|
||||||
|
"projectSummary": "Resumo do Projeto",
|
||||||
|
"total": "Total",
|
||||||
|
"validated": "Validados"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"projects": "Projetos",
|
||||||
|
"admin": "Admin"
|
||||||
|
},
|
||||||
|
"projectDropdown": {
|
||||||
|
"loading": "Carregando...",
|
||||||
|
"noProjectSelected": "Nenhum projeto selecionado",
|
||||||
|
"noProjectsAvailable": "Nenhum projeto disponível",
|
||||||
|
"createNewProject": "+ Criar Novo Projeto"
|
||||||
|
},
|
||||||
|
"noProjectWarning": {
|
||||||
|
"title": "Nenhum Projeto Selecionado",
|
||||||
|
"messagePart1": "Por favor, selecione um projeto no menu acima ou",
|
||||||
|
"createProject": "crie um novo projeto",
|
||||||
|
"messagePart2": "para começar."
|
||||||
|
},
|
||||||
|
"quickFilters": {
|
||||||
|
"title": "Filtros Rápidos de Busca",
|
||||||
|
"subtitle": "Clique em uma categoria para filtrar requisitos",
|
||||||
|
"noGroupsFound": "Nenhum grupo encontrado",
|
||||||
|
"total": "total"
|
||||||
|
},
|
||||||
|
"needsAttention": {
|
||||||
|
"title": "Precisa de Atenção",
|
||||||
|
"subtitle": "Requisitos com validação negada ou parcial",
|
||||||
|
"viewAll": "Ver Todos",
|
||||||
|
"moreRequirements": "mais requisitos precisam de atenção",
|
||||||
|
"by": "por"
|
||||||
|
},
|
||||||
|
"needsRevalidation": {
|
||||||
|
"title": "Precisa de Revalidação",
|
||||||
|
"subtitle": "Requisitos atualizados desde a última validação",
|
||||||
|
"viewAll": "Ver Todos",
|
||||||
|
"moreRequirements": "mais requisitos precisam de revalidação",
|
||||||
|
"versionBehind": "versão atrás",
|
||||||
|
"versionsBehind": "versões atrás",
|
||||||
|
"lastValidatedBy": "Última validação por",
|
||||||
|
"by": "por"
|
||||||
|
},
|
||||||
|
"allClear": {
|
||||||
|
"title": "Tudo Certo!",
|
||||||
|
"message": "Nenhum requisito precisa de atenção. Todas as validações estão aprovadas ou pendentes de revisão."
|
||||||
|
},
|
||||||
|
"createProject": {
|
||||||
|
"title": "Criar Novo Projeto",
|
||||||
|
"projectName": "Nome do Projeto",
|
||||||
|
"projectNamePlaceholder": "Digite o nome do projeto",
|
||||||
|
"description": "Descrição",
|
||||||
|
"descriptionPlaceholder": "Digite a descrição do projeto (opcional)",
|
||||||
|
"createButton": "Criar Projeto",
|
||||||
|
"creating": "Criando...",
|
||||||
|
"errorCreating": "Falha ao criar projeto. Por favor, tente novamente."
|
||||||
|
}
|
||||||
|
}
|
||||||
11
frontend/src/i18n/locales/pt/errors.json
Normal file
11
frontend/src/i18n/locales/pt/errors.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"generic": "Ocorreu um erro. Por favor, tente novamente.",
|
||||||
|
"loadFailed": "Falha ao carregar dados. Por favor, tente novamente.",
|
||||||
|
"saveFailed": "Falha ao salvar. Por favor, tente novamente.",
|
||||||
|
"deleteFailed": "Falha ao excluir. Por favor, tente novamente.",
|
||||||
|
"createFailed": "Falha ao criar. Por favor, tente novamente.",
|
||||||
|
"unauthorized": "Você não está autorizado a realizar esta ação.",
|
||||||
|
"notFound": "O recurso solicitado não foi encontrado.",
|
||||||
|
"networkError": "Erro de rede. Por favor, verifique sua conexão.",
|
||||||
|
"sessionExpired": "Sua sessão expirou. Por favor, faça login novamente."
|
||||||
|
}
|
||||||
5
frontend/src/i18n/locales/pt/home.json
Normal file
5
frontend/src/i18n/locales/pt/home.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"title": "Tabela Periódica de Requisitos",
|
||||||
|
"subtitle": "Gerencie e acompanhe os requisitos do seu projeto.",
|
||||||
|
"goToDashboard": "Ir para o Painel"
|
||||||
|
}
|
||||||
7
frontend/src/i18n/locales/pt/navbar.json
Normal file
7
frontend/src/i18n/locales/pt/navbar.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"brand": "Tabela Periódica de Requisitos",
|
||||||
|
"dashboard": "Painel",
|
||||||
|
"hello": "Olá",
|
||||||
|
"logout": "Sair",
|
||||||
|
"login": "Entrar"
|
||||||
|
}
|
||||||
155
frontend/src/i18n/locales/pt/requirementDetail.json
Normal file
155
frontend/src/i18n/locales/pt/requirementDetail.json
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
{
|
||||||
|
"pageTitle": "Ferramenta de Requisitos Digital Twin",
|
||||||
|
"breadcrumb": {
|
||||||
|
"search": "Buscar",
|
||||||
|
"details": "Detalhes"
|
||||||
|
},
|
||||||
|
"loadingRequirement": "Carregando requisito...",
|
||||||
|
"requirementNotFound": "Requisito não encontrado",
|
||||||
|
"noRequirementId": "Nenhum ID de requisito fornecido",
|
||||||
|
"backToRequirements": "Voltar aos Requisitos",
|
||||||
|
"errorLoading": "Falha ao carregar requisito. Por favor, tente novamente.",
|
||||||
|
"draft": {
|
||||||
|
"badge": "Rascunho - Não Finalizado",
|
||||||
|
"title": "Requisito em Rascunho",
|
||||||
|
"message": "Este requisito ainda está em status de rascunho e não foi finalizado. Pode estar sujeito a alterações."
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"description": "Descrição",
|
||||||
|
"relationships": "Relacionamentos",
|
||||||
|
"acceptanceCriteria": "Critérios de Aceitação",
|
||||||
|
"sharedComments": "Comentários Compartilhados",
|
||||||
|
"validate": "Validar",
|
||||||
|
"history": "Histórico"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"title": "Descrição",
|
||||||
|
"version": "Versão:",
|
||||||
|
"status": "Status:",
|
||||||
|
"validationStatus": "Status de Validação:",
|
||||||
|
"author": "Autor:",
|
||||||
|
"lastEditedBy": "Última Edição Por:",
|
||||||
|
"created": "Criado:",
|
||||||
|
"lastUpdated": "Última Atualização:",
|
||||||
|
"noDescription": "Nenhuma descrição fornecida.",
|
||||||
|
"editButton": "Editar"
|
||||||
|
},
|
||||||
|
"relationships": {
|
||||||
|
"title": "Relacionamentos",
|
||||||
|
"addButton": "Adicionar Relacionamento",
|
||||||
|
"noTypesWarning": "Nenhum tipo de relacionamento foi definido para este projeto. Contate um administrador para configurar os tipos de relacionamento.",
|
||||||
|
"loadingRelationships": "Carregando relacionamentos...",
|
||||||
|
"noRelationships": "Nenhum relacionamento definido ainda.",
|
||||||
|
"tableHeaders": {
|
||||||
|
"direction": "Direção",
|
||||||
|
"type": "Tipo",
|
||||||
|
"linkedRequirement": "Requisito Vinculado",
|
||||||
|
"createdBy": "Criado Por",
|
||||||
|
"date": "Data",
|
||||||
|
"actions": "Ações"
|
||||||
|
},
|
||||||
|
"outgoing": "→ Saída",
|
||||||
|
"incoming": "← Entrada"
|
||||||
|
},
|
||||||
|
"acceptanceCriteria": {
|
||||||
|
"title": "Critérios de Aceitação",
|
||||||
|
"noCriteria": "Nenhum critério de aceitação definido ainda.",
|
||||||
|
"addButton": "Adicionar Critério"
|
||||||
|
},
|
||||||
|
"comments": {
|
||||||
|
"title": "Comentários Compartilhados",
|
||||||
|
"placeholder": "Adicionar um comentário...",
|
||||||
|
"postButton": "Publicar Comentário",
|
||||||
|
"posting": "Publicando...",
|
||||||
|
"noComments": "Nenhum comentário ainda. Seja o primeiro a comentar!",
|
||||||
|
"loadingComments": "Carregando comentários...",
|
||||||
|
"reply": "Responder",
|
||||||
|
"cancelReply": "Cancelar Resposta",
|
||||||
|
"replyPlaceholder": "Escreva uma resposta...",
|
||||||
|
"postReply": "Publicar Resposta",
|
||||||
|
"errorPosting": "Falha ao publicar comentário. Por favor, tente novamente.",
|
||||||
|
"errorPostingReply": "Falha ao publicar resposta. Por favor, tente novamente.",
|
||||||
|
"roles": {
|
||||||
|
"editor": "Editor",
|
||||||
|
"auditor": "Auditor",
|
||||||
|
"admin": "Admin do Projeto",
|
||||||
|
"user": "Usuário"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"validate": {
|
||||||
|
"title": "Validar Requisito",
|
||||||
|
"currentStatus": "Status Atual:",
|
||||||
|
"requirementVersion": "Versão do Requisito:",
|
||||||
|
"lastValidatedBy": "Última validação por",
|
||||||
|
"staleWarning": "Este requisito foi modificado após a última validação (validado na versão {{validationVersion}}, versão atual {{currentVersion}}).",
|
||||||
|
"submitTitle": "Enviar Validação",
|
||||||
|
"statusLabel": "Status de Validação",
|
||||||
|
"selectStatus": "Selecione um status...",
|
||||||
|
"commentLabel": "Comentário",
|
||||||
|
"commentPlaceholder": "Adicione um comentário explicando sua decisão (opcional, mas recomendado)",
|
||||||
|
"submitButton": "Enviar Validação",
|
||||||
|
"submitting": "Enviando...",
|
||||||
|
"errorSubmitting": "Falha ao enviar validação. Por favor, tente novamente.",
|
||||||
|
"historyTitle": "Histórico de Validação",
|
||||||
|
"loadingHistory": "Carregando histórico...",
|
||||||
|
"noHistory": "Nenhum histórico de validação ainda.",
|
||||||
|
"tableHeaders": {
|
||||||
|
"date": "Data",
|
||||||
|
"status": "Status",
|
||||||
|
"version": "Versão",
|
||||||
|
"validator": "Validador",
|
||||||
|
"comment": "Comentário"
|
||||||
|
},
|
||||||
|
"noComment": "Sem comentário"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"title": "Histórico de Versões",
|
||||||
|
"originalAuthor": "Autor Original:",
|
||||||
|
"loadingHistory": "Carregando histórico...",
|
||||||
|
"noHistory": "Nenhum histórico ainda. O histórico é registrado quando o requisito é editado ou os relacionamentos mudam.",
|
||||||
|
"requirementEdited": "Requisito editado",
|
||||||
|
"linkCreated": "Link Criado",
|
||||||
|
"linkRemoved": "Link Removido",
|
||||||
|
"groupAdded": "Grupo Adicionado",
|
||||||
|
"groupRemoved": "Grupo Removido",
|
||||||
|
"noChangesDetected": "Nenhuma alteração visível detectada (pode ser uma alteração apenas de grupo).",
|
||||||
|
"showLess": "Mostrar menos",
|
||||||
|
"showFullDiff": "Mostrar diferença completa",
|
||||||
|
"deletedRequirement": "Requisito Excluído",
|
||||||
|
"unknownGroup": "Grupo Desconhecido"
|
||||||
|
},
|
||||||
|
"addRelationshipModal": {
|
||||||
|
"title": "Adicionar Relacionamento",
|
||||||
|
"relationshipType": "Tipo de Relacionamento",
|
||||||
|
"selectType": "Selecione um tipo de relacionamento...",
|
||||||
|
"targetRequirement": "Requisito de Destino",
|
||||||
|
"searchPlaceholder": "Buscar por código de tag ou nome...",
|
||||||
|
"selected": "Selecionado:",
|
||||||
|
"createButton": "Criar Relacionamento",
|
||||||
|
"errorCreating": "Falha ao criar link. Por favor, tente novamente."
|
||||||
|
},
|
||||||
|
"editModal": {
|
||||||
|
"title": "Editar Requisito",
|
||||||
|
"loadingOptions": "Carregando opções...",
|
||||||
|
"name": "Nome",
|
||||||
|
"tag": "Tag",
|
||||||
|
"selectTag": "Selecione uma tag...",
|
||||||
|
"priority": "Prioridade",
|
||||||
|
"noPriority": "Sem prioridade",
|
||||||
|
"status": "Status",
|
||||||
|
"draftNote": "Requisitos em rascunho não estão finalizados e são marcados com um indicador visual.",
|
||||||
|
"description": "Descrição",
|
||||||
|
"groups": "Grupos",
|
||||||
|
"noGroupsAvailable": "Nenhum grupo disponível",
|
||||||
|
"errorSaving": "Falha ao salvar alterações. Por favor, tente novamente.",
|
||||||
|
"nameTagRequired": "Nome e Tag são obrigatórios"
|
||||||
|
},
|
||||||
|
"deleteModal": {
|
||||||
|
"deleteComment": "Excluir Comentário",
|
||||||
|
"deleteReply": "Excluir Resposta",
|
||||||
|
"deleteRelationship": "Excluir Relacionamento",
|
||||||
|
"confirmDeleteComment": "Tem certeza de que deseja excluir este comentário? Isso também ocultará todas as respostas.",
|
||||||
|
"confirmDeleteReply": "Tem certeza de que deseja excluir esta resposta?",
|
||||||
|
"confirmDeleteRelationship": "Tem certeza de que deseja excluir este relacionamento? Esta ação não pode ser desfeita."
|
||||||
|
}
|
||||||
|
}
|
||||||
87
frontend/src/i18n/locales/pt/requirements.json
Normal file
87
frontend/src/i18n/locales/pt/requirements.json
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"title": "Ferramenta de Requisitos Digital Twin"
|
||||||
|
},
|
||||||
|
"breadcrumb": {
|
||||||
|
"projects": "Projetos",
|
||||||
|
"searchRequirements": "Buscar Requisitos"
|
||||||
|
},
|
||||||
|
"noProjectSelected": {
|
||||||
|
"title": "Nenhum Projeto Selecionado",
|
||||||
|
"message": "Por favor, selecione um projeto no painel para visualizar os requisitos.",
|
||||||
|
"goToDashboard": "Ir para o Painel"
|
||||||
|
},
|
||||||
|
"loadingRequirements": "Carregando requisitos...",
|
||||||
|
"newRequirement": "Novo Requisito",
|
||||||
|
"deleted": "Excluídos",
|
||||||
|
"searchPlaceholder": "Buscar por código de tag ou título do requisito",
|
||||||
|
"noRequirementsFound": "Nenhum requisito encontrado com os critérios selecionados.",
|
||||||
|
"filters": {
|
||||||
|
"filterGroup": "Filtrar Grupo",
|
||||||
|
"filterValidationStatus": "Filtrar Status de Validação",
|
||||||
|
"showingNeedsRevalidation": "Mostrando: Precisa de Revalidação",
|
||||||
|
"showingNeedsAttention": "Mostrando: Precisa de Atenção",
|
||||||
|
"needsRevalidation": "Precisa de Revalidação",
|
||||||
|
"statuses": {
|
||||||
|
"approved": "Aprovado",
|
||||||
|
"denied": "Negado",
|
||||||
|
"partiallyApproved": "Parcialmente Aprovado",
|
||||||
|
"notValidated": "Não Validado"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"orderBy": {
|
||||||
|
"label": "Ordenar por",
|
||||||
|
"date": "Data",
|
||||||
|
"priority": "Prioridade",
|
||||||
|
"name": "Nome"
|
||||||
|
},
|
||||||
|
"caption": {
|
||||||
|
"title": "Legenda das Tags",
|
||||||
|
"viewCaptions": "Ver legendas das tags"
|
||||||
|
},
|
||||||
|
"requirement": {
|
||||||
|
"draft": "Rascunho",
|
||||||
|
"draftTooltip": "Este requisito ainda está em rascunho e não foi finalizado",
|
||||||
|
"noGroups": "Sem grupos",
|
||||||
|
"more": "mais",
|
||||||
|
"stale": "Desatualizado",
|
||||||
|
"staleTooltip": "O requisito foi modificado após a validação",
|
||||||
|
"by": "por",
|
||||||
|
"priority": "Prioridade",
|
||||||
|
"version": "Versão"
|
||||||
|
},
|
||||||
|
"deletedPanel": {
|
||||||
|
"title": "Requisitos Excluídos",
|
||||||
|
"noDeleted": "Nenhum requisito excluído encontrado.",
|
||||||
|
"deletedWillAppear": "Requisitos excluídos aparecerão aqui.",
|
||||||
|
"unnamed": "Requisito sem nome",
|
||||||
|
"originalId": "ID Original",
|
||||||
|
"deletedAt": "Excluído",
|
||||||
|
"deletedBy": "Excluído por",
|
||||||
|
"footerNote": "Requisitos excluídos são preservados no histórico para fins de auditoria."
|
||||||
|
},
|
||||||
|
"deleteModal": {
|
||||||
|
"title": "Excluir Requisito",
|
||||||
|
"confirmMessage": "Tem certeza de que deseja excluir o requisito",
|
||||||
|
"explanation": "Esta ação moverá o requisito para os itens excluídos. Você pode visualizar os requisitos excluídos no painel \"Excluídos\".",
|
||||||
|
"deleting": "Excluindo..."
|
||||||
|
},
|
||||||
|
"createModal": {
|
||||||
|
"title": "Novo Requisito",
|
||||||
|
"tag": "Tag",
|
||||||
|
"selectTag": "Selecione uma tag...",
|
||||||
|
"name": "Nome",
|
||||||
|
"namePlaceholder": "Digite o nome do requisito",
|
||||||
|
"description": "Descrição",
|
||||||
|
"descriptionPlaceholder": "Digite a descrição do requisito (opcional)",
|
||||||
|
"priority": "Prioridade",
|
||||||
|
"selectPriority": "Selecione uma prioridade (opcional)...",
|
||||||
|
"groups": "Grupos",
|
||||||
|
"createButton": "Criar Requisito",
|
||||||
|
"creating": "Criando...",
|
||||||
|
"errorCreating": "Falha ao criar requisito. Por favor, tente novamente.",
|
||||||
|
"noProjectSelected": "Nenhum projeto selecionado",
|
||||||
|
"nameRequired": "O nome do requisito é obrigatório",
|
||||||
|
"tagRequired": "Por favor, selecione uma tag"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
frontend/src/i18n/locales/pt/validation.json
Normal file
7
frontend/src/i18n/locales/pt/validation.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"approved": "Aprovado",
|
||||||
|
"denied": "Negado",
|
||||||
|
"partial": "Parcial",
|
||||||
|
"partiallyApproved": "Parcialmente Aprovado",
|
||||||
|
"notValidated": "Não Validado"
|
||||||
|
}
|
||||||
@@ -5,6 +5,9 @@ import App from './App'
|
|||||||
import { AuthProvider, ProjectProvider } from '@/context'
|
import { AuthProvider, ProjectProvider } from '@/context'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
|
||||||
|
// Initialize i18n before rendering
|
||||||
|
import './i18n'
|
||||||
|
|
||||||
// Global fetch interceptor to handle 401 Unauthorized responses
|
// Global fetch interceptor to handle 401 Unauthorized responses
|
||||||
// This intercepts all fetch calls and attempts silent refresh before redirecting
|
// This intercepts all fetch calls and attempts silent refresh before redirecting
|
||||||
const originalFetch = window.fetch
|
const originalFetch = window.fetch
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useAuth, useProject } from '@/hooks'
|
import { useAuth, useProject } from '@/hooks'
|
||||||
import {
|
import {
|
||||||
projectService,
|
projectService,
|
||||||
@@ -13,6 +14,8 @@ import {
|
|||||||
type TabType = 'project' | 'members' | 'relationships'
|
type TabType = 'project' | 'members' | 'relationships'
|
||||||
|
|
||||||
export default function AdminPage() {
|
export default function AdminPage() {
|
||||||
|
const { t } = useTranslation('admin')
|
||||||
|
const { t: tCommon } = useTranslation('common')
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
const { currentProject, setCurrentProject } = useProject()
|
const { currentProject, setCurrentProject } = useProject()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -291,13 +294,13 @@ export default function AdminPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white flex items-center justify-center">
|
<div className="min-h-screen bg-white flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h2 className="text-xl font-semibold text-gray-700 mb-2">No Project Selected</h2>
|
<h2 className="text-xl font-semibold text-gray-700 mb-2">{t('noProject.title')}</h2>
|
||||||
<p className="text-gray-500 mb-4">Please select a project from the dashboard first.</p>
|
<p className="text-gray-500 mb-4">{t('noProject.message')}</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/dashboard')}
|
onClick={() => navigate('/dashboard')}
|
||||||
className="px-4 py-2 bg-teal-600 text-white rounded hover:bg-teal-700"
|
className="px-4 py-2 bg-teal-600 text-white rounded hover:bg-teal-700"
|
||||||
>
|
>
|
||||||
Go to Dashboard
|
{t('noProject.goToDashboard')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -308,9 +311,9 @@ export default function AdminPage() {
|
|||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-white">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="py-6 text-center border-b border-gray-200">
|
<header className="py-6 text-center border-b border-gray-200">
|
||||||
<h1 className="text-3xl font-semibold text-teal-700">Admin Panel</h1>
|
<h1 className="text-3xl font-semibold text-teal-700">{t('pageTitle')}</h1>
|
||||||
<p className="text-gray-500 mt-1">
|
<p className="text-gray-500 mt-1">
|
||||||
Managing: <span className="font-medium">{currentProject.project_name}</span>
|
{t('managing')} <span className="font-medium">{currentProject.project_name}</span>
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -326,7 +329,7 @@ export default function AdminPage() {
|
|||||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Project Settings
|
{t('tabs.projectSettings')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('members')}
|
onClick={() => setActiveTab('members')}
|
||||||
@@ -336,7 +339,7 @@ export default function AdminPage() {
|
|||||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Member Roles
|
{t('tabs.memberRoles')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('relationships')}
|
onClick={() => setActiveTab('relationships')}
|
||||||
@@ -346,7 +349,7 @@ export default function AdminPage() {
|
|||||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Relationship Types
|
{t('tabs.relationshipTypes')}
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
@@ -361,7 +364,7 @@ export default function AdminPage() {
|
|||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||||
</svg>
|
</svg>
|
||||||
Back to Dashboard
|
{t('backToDashboard')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -370,7 +373,7 @@ export default function AdminPage() {
|
|||||||
{/* Project Settings Tab */}
|
{/* Project Settings Tab */}
|
||||||
{activeTab === 'project' && (
|
{activeTab === 'project' && (
|
||||||
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
||||||
<h2 className="text-xl font-semibold text-gray-800 mb-6">Project Settings</h2>
|
<h2 className="text-xl font-semibold text-gray-800 mb-6">{t('projectSettings.title')}</h2>
|
||||||
|
|
||||||
<form onSubmit={handleProjectUpdate} className="space-y-6">
|
<form onSubmit={handleProjectUpdate} className="space-y-6">
|
||||||
{projectError && (
|
{projectError && (
|
||||||
@@ -387,7 +390,7 @@ export default function AdminPage() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Project Name <span className="text-red-500">*</span>
|
{t('projectSettings.projectName')} <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -400,7 +403,7 @@ export default function AdminPage() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Description
|
{t('projectSettings.description')}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={projectDesc}
|
value={projectDesc}
|
||||||
@@ -416,7 +419,7 @@ export default function AdminPage() {
|
|||||||
disabled={projectLoading}
|
disabled={projectLoading}
|
||||||
className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50"
|
className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{projectLoading ? 'Saving...' : 'Save Changes'}
|
{projectLoading ? tCommon('saving') : t('projectSettings.saveButton')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -426,7 +429,7 @@ export default function AdminPage() {
|
|||||||
{/* Members Tab */}
|
{/* Members Tab */}
|
||||||
{activeTab === 'members' && (
|
{activeTab === 'members' && (
|
||||||
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
||||||
<h2 className="text-xl font-semibold text-gray-800 mb-6">Member Roles</h2>
|
<h2 className="text-xl font-semibold text-gray-800 mb-6">{t('memberRoles.title')}</h2>
|
||||||
|
|
||||||
{membersError && (
|
{membersError && (
|
||||||
<div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded text-sm">
|
<div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded text-sm">
|
||||||
@@ -435,17 +438,17 @@ export default function AdminPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{membersLoading ? (
|
{membersLoading ? (
|
||||||
<div className="text-center py-8 text-gray-500">Loading members...</div>
|
<div className="text-center py-8 text-gray-500">{t('memberRoles.loadingMembers')}</div>
|
||||||
) : members.length === 0 ? (
|
) : members.length === 0 ? (
|
||||||
<div className="text-center py-8 text-gray-500">No members found</div>
|
<div className="text-center py-8 text-gray-500">{t('memberRoles.noMembers')}</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-gray-200">
|
<tr className="border-b border-gray-200">
|
||||||
<th className="text-left py-3 px-4 font-medium text-gray-700">User</th>
|
<th className="text-left py-3 px-4 font-medium text-gray-700">{t('memberRoles.tableHeaders.user')}</th>
|
||||||
<th className="text-left py-3 px-4 font-medium text-gray-700">Role</th>
|
<th className="text-left py-3 px-4 font-medium text-gray-700">{t('memberRoles.tableHeaders.role')}</th>
|
||||||
<th className="text-left py-3 px-4 font-medium text-gray-700">Actions</th>
|
<th className="text-left py-3 px-4 font-medium text-gray-700">{t('memberRoles.tableHeaders.actions')}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -459,7 +462,7 @@ export default function AdminPage() {
|
|||||||
<span className="text-sm text-gray-900">{member.sub}</span>
|
<span className="text-sm text-gray-900">{member.sub}</span>
|
||||||
{isCurrentUser && (
|
{isCurrentUser && (
|
||||||
<span className="text-xs bg-teal-100 text-teal-700 px-2 py-0.5 rounded">
|
<span className="text-xs bg-teal-100 text-teal-700 px-2 py-0.5 rounded">
|
||||||
You
|
{t('memberRoles.youBadge')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -485,7 +488,7 @@ export default function AdminPage() {
|
|||||||
className={`px-3 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 ${
|
className={`px-3 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 ${
|
||||||
isCurrentUser && member.role_id === 3 ? 'bg-gray-100 cursor-not-allowed' : ''
|
isCurrentUser && member.role_id === 3 ? 'bg-gray-100 cursor-not-allowed' : ''
|
||||||
}`}
|
}`}
|
||||||
title={isCurrentUser && member.role_id === 3 ? 'You cannot demote yourself' : ''}
|
title={isCurrentUser && member.role_id === 3 ? t('memberRoles.cannotDemoteSelf') : ''}
|
||||||
>
|
>
|
||||||
{roles.map((role) => (
|
{roles.map((role) => (
|
||||||
<option key={role.id} value={role.id}>
|
<option key={role.id} value={role.id}>
|
||||||
@@ -494,7 +497,7 @@ export default function AdminPage() {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
{updatingMember === member.id && (
|
{updatingMember === member.id && (
|
||||||
<span className="ml-2 text-xs text-gray-500">Saving...</span>
|
<span className="ml-2 text-xs text-gray-500">{tCommon('saving')}</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -511,7 +514,7 @@ export default function AdminPage() {
|
|||||||
{activeTab === 'relationships' && (
|
{activeTab === 'relationships' && (
|
||||||
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<h2 className="text-xl font-semibold text-gray-800">Relationship Types</h2>
|
<h2 className="text-xl font-semibold text-gray-800">{t('relationshipTypes.title')}</h2>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
resetRelTypeForm()
|
resetRelTypeForm()
|
||||||
@@ -519,7 +522,7 @@ export default function AdminPage() {
|
|||||||
}}
|
}}
|
||||||
className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700"
|
className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700"
|
||||||
>
|
>
|
||||||
+ Add Type
|
{t('relationshipTypes.addButton')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -530,10 +533,10 @@ export default function AdminPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{relTypesLoading ? (
|
{relTypesLoading ? (
|
||||||
<div className="text-center py-8 text-gray-500">Loading relationship types...</div>
|
<div className="text-center py-8 text-gray-500">{t('relationshipTypes.loadingTypes')}</div>
|
||||||
) : relationshipTypes.length === 0 ? (
|
) : relationshipTypes.length === 0 ? (
|
||||||
<div className="text-center py-8 text-gray-500">
|
<div className="text-center py-8 text-gray-500">
|
||||||
No relationship types defined yet. Create one to link requirements.
|
{t('relationshipTypes.noTypes')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@@ -546,7 +549,7 @@ export default function AdminPage() {
|
|||||||
<div className="font-medium text-gray-900">{relType.type_name}</div>
|
<div className="font-medium text-gray-900">{relType.type_name}</div>
|
||||||
{relType.inverse_type_name && (
|
{relType.inverse_type_name && (
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
Inverse: {relType.inverse_type_name}
|
{t('relationshipTypes.inverse')} {relType.inverse_type_name}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{relType.type_description && (
|
{relType.type_description && (
|
||||||
@@ -558,13 +561,13 @@ export default function AdminPage() {
|
|||||||
onClick={() => openEditModal(relType)}
|
onClick={() => openEditModal(relType)}
|
||||||
className="px-3 py-1.5 border border-gray-300 rounded text-sm text-gray-700 hover:bg-gray-100"
|
className="px-3 py-1.5 border border-gray-300 rounded text-sm text-gray-700 hover:bg-gray-100"
|
||||||
>
|
>
|
||||||
Edit
|
{t('relationshipTypes.editButton')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => openDeleteModal(relType)}
|
onClick={() => openDeleteModal(relType)}
|
||||||
className="px-3 py-1.5 border border-red-300 rounded text-sm text-red-600 hover:bg-red-50"
|
className="px-3 py-1.5 border border-red-300 rounded text-sm text-red-600 hover:bg-red-50"
|
||||||
>
|
>
|
||||||
Delete
|
{t('relationshipTypes.deleteButton')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -580,7 +583,7 @@ export default function AdminPage() {
|
|||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
|
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
||||||
<h2 className="text-xl font-semibold text-gray-800">Create Relationship Type</h2>
|
<h2 className="text-xl font-semibold text-gray-800">{t('createRelTypeModal.title')}</h2>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowCreateRelTypeModal(false)
|
setShowCreateRelTypeModal(false)
|
||||||
@@ -604,13 +607,13 @@ export default function AdminPage() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Type Name <span className="text-red-500">*</span>
|
{t('createRelTypeModal.typeName')} <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={relTypeName}
|
value={relTypeName}
|
||||||
onChange={(e) => setRelTypeName(e.target.value)}
|
onChange={(e) => setRelTypeName(e.target.value)}
|
||||||
placeholder="e.g., Depends On"
|
placeholder={t('createRelTypeModal.typeNamePlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -618,23 +621,23 @@ export default function AdminPage() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Inverse Name
|
{t('createRelTypeModal.inverseName')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={relTypeInverse}
|
value={relTypeInverse}
|
||||||
onChange={(e) => setRelTypeInverse(e.target.value)}
|
onChange={(e) => setRelTypeInverse(e.target.value)}
|
||||||
placeholder="e.g., Depended By"
|
placeholder={t('createRelTypeModal.inverseNamePlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
Optional. The name shown when viewing from the target requirement.
|
{t('createRelTypeModal.inverseNameHint')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Description
|
{t('createRelTypeModal.description')}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={relTypeDesc}
|
value={relTypeDesc}
|
||||||
@@ -655,14 +658,14 @@ export default function AdminPage() {
|
|||||||
className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-100"
|
className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-100"
|
||||||
disabled={relTypeFormLoading}
|
disabled={relTypeFormLoading}
|
||||||
>
|
>
|
||||||
Cancel
|
{tCommon('cancel')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50"
|
className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50"
|
||||||
disabled={relTypeFormLoading}
|
disabled={relTypeFormLoading}
|
||||||
>
|
>
|
||||||
{relTypeFormLoading ? 'Creating...' : 'Create'}
|
{relTypeFormLoading ? tCommon('creating') : t('createRelTypeModal.createButton')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -675,7 +678,7 @@ export default function AdminPage() {
|
|||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
|
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
||||||
<h2 className="text-xl font-semibold text-gray-800">Edit Relationship Type</h2>
|
<h2 className="text-xl font-semibold text-gray-800">{t('editRelTypeModal.title')}</h2>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowEditRelTypeModal(false)
|
setShowEditRelTypeModal(false)
|
||||||
@@ -700,7 +703,7 @@ export default function AdminPage() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Type Name <span className="text-red-500">*</span>
|
{t('createRelTypeModal.typeName')} <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -713,7 +716,7 @@ export default function AdminPage() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Inverse Name
|
{t('createRelTypeModal.inverseName')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -725,7 +728,7 @@ export default function AdminPage() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Description
|
{t('createRelTypeModal.description')}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={relTypeDesc}
|
value={relTypeDesc}
|
||||||
@@ -747,14 +750,14 @@ export default function AdminPage() {
|
|||||||
className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-100"
|
className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-100"
|
||||||
disabled={relTypeFormLoading}
|
disabled={relTypeFormLoading}
|
||||||
>
|
>
|
||||||
Cancel
|
{tCommon('cancel')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50"
|
className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50"
|
||||||
disabled={relTypeFormLoading}
|
disabled={relTypeFormLoading}
|
||||||
>
|
>
|
||||||
{relTypeFormLoading ? 'Saving...' : 'Save Changes'}
|
{relTypeFormLoading ? tCommon('saving') : t('editRelTypeModal.saveButton')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -767,7 +770,7 @@ export default function AdminPage() {
|
|||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
|
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
|
||||||
<div className="px-6 py-4 border-b border-gray-200">
|
<div className="px-6 py-4 border-b border-gray-200">
|
||||||
<h2 className="text-xl font-semibold text-gray-800">Delete Relationship Type</h2>
|
<h2 className="text-xl font-semibold text-gray-800">{t('deleteRelTypeModal.title')}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-6 py-4">
|
<div className="px-6 py-4">
|
||||||
@@ -778,11 +781,11 @@ export default function AdminPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-gray-700">
|
<p className="text-gray-700">
|
||||||
Are you sure you want to delete the relationship type{' '}
|
{t('deleteRelTypeModal.confirmMessage')}{' '}
|
||||||
<span className="font-semibold">"{selectedRelType.type_name}"</span>?
|
<span className="font-semibold">"{selectedRelType.type_name}"</span>?
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-red-600 mt-2">
|
<p className="text-sm text-red-600 mt-2">
|
||||||
⚠️ This will also delete all requirement links using this type.
|
{t('deleteRelTypeModal.warningMessage')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -796,14 +799,14 @@ export default function AdminPage() {
|
|||||||
className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-100"
|
className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-100"
|
||||||
disabled={relTypeFormLoading}
|
disabled={relTypeFormLoading}
|
||||||
>
|
>
|
||||||
Cancel
|
{tCommon('cancel')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleDeleteRelType}
|
onClick={handleDeleteRelType}
|
||||||
className="px-4 py-2 bg-red-600 text-white rounded text-sm font-medium hover:bg-red-700 disabled:opacity-50"
|
className="px-4 py-2 bg-red-600 text-white rounded text-sm font-medium hover:bg-red-700 disabled:opacity-50"
|
||||||
disabled={relTypeFormLoading}
|
disabled={relTypeFormLoading}
|
||||||
>
|
>
|
||||||
{relTypeFormLoading ? 'Deleting...' : 'Delete'}
|
{relTypeFormLoading ? tCommon('deleting') : t('deleteRelTypeModal.deleteButton')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useAuth, useProject } from '@/hooks'
|
import { useAuth, useProject } from '@/hooks'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { groupService, requirementService, Group } from '@/services'
|
import { groupService, requirementService, Group } from '@/services'
|
||||||
|
import { LanguageSelector } from '@/components'
|
||||||
import type { Requirement } from '@/services/requirementService'
|
import type { Requirement } from '@/services/requirementService'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,6 +75,8 @@ export default function DashboardPage() {
|
|||||||
const { user, logout } = useAuth()
|
const { user, logout } = useAuth()
|
||||||
const { projects, currentProject, setCurrentProject, isLoading: projectsLoading, createProject } = useProject()
|
const { projects, currentProject, setCurrentProject, isLoading: projectsLoading, createProject } = useProject()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { t } = useTranslation('dashboard')
|
||||||
|
const { t: tCommon } = useTranslation('common')
|
||||||
const [groups, setGroups] = useState<Group[]>([])
|
const [groups, setGroups] = useState<Group[]>([])
|
||||||
const [requirements, setRequirements] = useState<Requirement[]>([])
|
const [requirements, setRequirements] = useState<Requirement[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
@@ -235,9 +239,9 @@ export default function DashboardPage() {
|
|||||||
{/* Sidebar Header */}
|
{/* Sidebar Header */}
|
||||||
<div className="p-6 border-b border-slate-700">
|
<div className="p-6 border-b border-slate-700">
|
||||||
<h1 className="text-lg font-semibold text-teal-400">
|
<h1 className="text-lg font-semibold text-teal-400">
|
||||||
Digital Twin
|
{t('sidebar.title')}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-slate-400">Requirements Tool</p>
|
<p className="text-sm text-slate-400">{t('sidebar.subtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
@@ -250,11 +254,11 @@ export default function DashboardPage() {
|
|||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||||
</svg>
|
</svg>
|
||||||
Create Requirement
|
{t('sidebar.createRequirement')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="pt-4 pb-2">
|
<div className="pt-4 pb-2">
|
||||||
<p className="px-4 text-xs font-semibold text-slate-500 uppercase tracking-wider">Navigation</p>
|
<p className="px-4 text-xs font-semibold text-slate-500 uppercase tracking-wider">{t('sidebar.navigation')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search Requirements */}
|
{/* Search Requirements */}
|
||||||
@@ -265,7 +269,7 @@ export default function DashboardPage() {
|
|||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
Search Requirements
|
{t('sidebar.searchRequirements')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* My Requirements */}
|
{/* My Requirements */}
|
||||||
@@ -276,7 +280,7 @@ export default function DashboardPage() {
|
|||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||||
</svg>
|
</svg>
|
||||||
My Requirements
|
{t('sidebar.myRequirements')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Reports */}
|
{/* Reports */}
|
||||||
@@ -286,22 +290,22 @@ export default function DashboardPage() {
|
|||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
</svg>
|
</svg>
|
||||||
Generate Report
|
{t('sidebar.generateReport')}
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Project Stats Summary at bottom */}
|
{/* Project Stats Summary at bottom */}
|
||||||
{currentProject && !loading && (
|
{currentProject && !loading && (
|
||||||
<div className="absolute bottom-0 left-0 w-64 p-4 border-t border-slate-700 bg-slate-900">
|
<div className="absolute bottom-0 left-0 w-64 p-4 border-t border-slate-700 bg-slate-900">
|
||||||
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3">Project Summary</p>
|
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3">{t('sidebar.projectSummary')}</p>
|
||||||
<div className="grid grid-cols-2 gap-2 text-center">
|
<div className="grid grid-cols-2 gap-2 text-center">
|
||||||
<div className="bg-slate-800 rounded p-2">
|
<div className="bg-slate-800 rounded p-2">
|
||||||
<p className="text-xl font-bold text-white">{getTotalStats().total}</p>
|
<p className="text-xl font-bold text-white">{getTotalStats().total}</p>
|
||||||
<p className="text-xs text-slate-400">Total</p>
|
<p className="text-xs text-slate-400">{t('sidebar.total')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-slate-800 rounded p-2">
|
<div className="bg-slate-800 rounded p-2">
|
||||||
<p className="text-xl font-bold text-green-400">{getTotalStats().validated}</p>
|
<p className="text-xl font-bold text-green-400">{getTotalStats().validated}</p>
|
||||||
<p className="text-xs text-slate-400">Validated</p>
|
<p className="text-xs text-slate-400">{t('sidebar.validated')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -315,7 +319,7 @@ export default function DashboardPage() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
{/* Breadcrumb with Project Dropdown */}
|
{/* Breadcrumb with Project Dropdown */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm text-gray-500">Projects</span>
|
<span className="text-sm text-gray-500">{t('header.projects')}</span>
|
||||||
<svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -327,7 +331,7 @@ export default function DashboardPage() {
|
|||||||
className="flex items-center gap-2 text-sm font-semibold text-gray-900 hover:text-teal-700 focus:outline-none"
|
className="flex items-center gap-2 text-sm font-semibold text-gray-900 hover:text-teal-700 focus:outline-none"
|
||||||
>
|
>
|
||||||
{projectsLoading ? (
|
{projectsLoading ? (
|
||||||
<span className="text-gray-500">Loading...</span>
|
<span className="text-gray-500">{tCommon('loading')}</span>
|
||||||
) : currentProject ? (
|
) : currentProject ? (
|
||||||
<>
|
<>
|
||||||
{currentProject.project_name}
|
{currentProject.project_name}
|
||||||
@@ -336,7 +340,7 @@ export default function DashboardPage() {
|
|||||||
</svg>
|
</svg>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-gray-500 italic">No project selected</span>
|
<span className="text-gray-500 italic">{t('projectDropdown.noProjectSelected')}</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -346,7 +350,7 @@ export default function DashboardPage() {
|
|||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
{projects.length === 0 ? (
|
{projects.length === 0 ? (
|
||||||
<div className="px-4 py-2 text-sm text-gray-500">
|
<div className="px-4 py-2 text-sm text-gray-500">
|
||||||
No projects available
|
{t('projectDropdown.noProjectsAvailable')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
projects.map((project) => (
|
projects.map((project) => (
|
||||||
@@ -376,7 +380,7 @@ export default function DashboardPage() {
|
|||||||
}}
|
}}
|
||||||
className="w-full text-left px-4 py-2 text-sm text-teal-600 hover:bg-gray-100 font-medium"
|
className="w-full text-left px-4 py-2 text-sm text-teal-600 hover:bg-gray-100 font-medium"
|
||||||
>
|
>
|
||||||
+ Create New Project
|
{t('projectDropdown.createNewProject')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -386,14 +390,8 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
{/* Right side utilities - grouped tighter */}
|
{/* Right side utilities - grouped tighter */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{/* Language Toggle */}
|
{/* Language Selector */}
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
<LanguageSelector compact />
|
||||||
<span>EN</span>
|
|
||||||
<div className="relative inline-flex h-5 w-9 items-center rounded-full bg-gray-300 cursor-pointer">
|
|
||||||
<span className="inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow-sm translate-x-0.5" />
|
|
||||||
</div>
|
|
||||||
<span>PT</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
<div className="h-6 w-px bg-gray-300"></div>
|
<div className="h-6 w-px bg-gray-300"></div>
|
||||||
@@ -408,7 +406,7 @@ export default function DashboardPage() {
|
|||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
Admin
|
{t('header.admin')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -422,15 +420,15 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
<p className="font-medium text-gray-700">
|
<p className="font-medium text-gray-700">
|
||||||
{user?.full_name || user?.preferred_username || 'User'}
|
{user?.full_name || user?.preferred_username || tCommon('user')}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500">{user?.role || 'user'}</p>
|
<p className="text-xs text-gray-500">{user?.role || tCommon('user').toLowerCase()}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={logout}
|
onClick={logout}
|
||||||
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
title="Logout"
|
title={tCommon('logout')}
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||||
@@ -451,16 +449,16 @@ export default function DashboardPage() {
|
|||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
</svg>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-amber-800">No Project Selected</h3>
|
<h3 className="font-semibold text-amber-800">{t('noProjectWarning.title')}</h3>
|
||||||
<p className="text-sm text-amber-700">
|
<p className="text-sm text-amber-700">
|
||||||
Please select a project from the dropdown above or{' '}
|
{t('noProjectWarning.messagePart1')}{' '}
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowCreateProjectModal(true)}
|
onClick={() => setShowCreateProjectModal(true)}
|
||||||
className="underline font-medium hover:text-amber-900"
|
className="underline font-medium hover:text-amber-900"
|
||||||
>
|
>
|
||||||
create a new project
|
{t('noProjectWarning.createProject')}
|
||||||
</button>
|
</button>
|
||||||
{' '}to get started.
|
{' '}{t('noProjectWarning.messagePart2')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -471,10 +469,10 @@ export default function DashboardPage() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<h2 className="text-xl font-semibold text-gray-800">
|
<h2 className="text-xl font-semibold text-gray-800">
|
||||||
Quick Search Filters
|
{t('quickFilters.title')}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Click a category to filter requirements
|
{t('quickFilters.subtitle')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -483,7 +481,7 @@ export default function DashboardPage() {
|
|||||||
<div className="flex justify-center items-center min-h-[200px]">
|
<div className="flex justify-center items-center min-h-[200px]">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="animate-spin rounded-full h-10 w-10 border-b-2 border-teal-600 mx-auto"></div>
|
<div className="animate-spin rounded-full h-10 w-10 border-b-2 border-teal-600 mx-auto"></div>
|
||||||
<p className="mt-3 text-gray-500">Loading...</p>
|
<p className="mt-3 text-gray-500">{tCommon('loading')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -556,7 +554,7 @@ export default function DashboardPage() {
|
|||||||
<span
|
<span
|
||||||
className={`ml-auto text-xs ${isLightText ? 'text-white/70' : 'text-black/50'}`}
|
className={`ml-auto text-xs ${isLightText ? 'text-white/70' : 'text-black/50'}`}
|
||||||
>
|
>
|
||||||
{stats.total} total
|
{stats.total} {t('quickFilters.total')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -569,7 +567,7 @@ export default function DashboardPage() {
|
|||||||
{/* Empty State */}
|
{/* Empty State */}
|
||||||
{!loading && !error && groups.length === 0 && (
|
{!loading && !error && groups.length === 0 && (
|
||||||
<div className="flex justify-center items-center min-h-[200px]">
|
<div className="flex justify-center items-center min-h-[200px]">
|
||||||
<div className="text-gray-500">No groups found</div>
|
<div className="text-gray-500">{t('quickFilters.noGroupsFound')}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -586,10 +584,10 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-gray-800">
|
<h2 className="text-lg font-semibold text-gray-800">
|
||||||
Needs Attention
|
{t('needsAttention.title')}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Requirements with denied or partial validation
|
{t('needsAttention.subtitle')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -597,7 +595,7 @@ export default function DashboardPage() {
|
|||||||
onClick={handleNeedsAttentionClick}
|
onClick={handleNeedsAttentionClick}
|
||||||
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-amber-700 bg-amber-50 hover:bg-amber-100 rounded-lg transition-colors"
|
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-amber-700 bg-amber-50 hover:bg-amber-100 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
View All ({requirementsNeedingAttention.length})
|
{t('needsAttention.viewAll')} ({requirementsNeedingAttention.length})
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -653,7 +651,7 @@ export default function DashboardPage() {
|
|||||||
onClick={handleNeedsAttentionClick}
|
onClick={handleNeedsAttentionClick}
|
||||||
className="text-sm text-amber-600 hover:text-amber-700 font-medium"
|
className="text-sm text-amber-600 hover:text-amber-700 font-medium"
|
||||||
>
|
>
|
||||||
+ {requirementsNeedingAttention.length - 6} more requirements need attention
|
+ {requirementsNeedingAttention.length - 6} {t('needsAttention.moreRequirements')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -672,10 +670,10 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-gray-800">
|
<h2 className="text-lg font-semibold text-gray-800">
|
||||||
Needs Revalidation
|
{t('needsRevalidation.title')}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Requirements updated since last validation
|
{t('needsRevalidation.subtitle')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -683,7 +681,7 @@ export default function DashboardPage() {
|
|||||||
onClick={handleNeedsRevalidationClick}
|
onClick={handleNeedsRevalidationClick}
|
||||||
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-blue-700 bg-blue-50 hover:bg-blue-100 rounded-lg transition-colors"
|
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-blue-700 bg-blue-50 hover:bg-blue-100 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
View All ({requirementsNeedingRevalidation.length})
|
{t('needsRevalidation.viewAll')} ({requirementsNeedingRevalidation.length})
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -706,7 +704,7 @@ export default function DashboardPage() {
|
|||||||
{req.tag.tag_code}
|
{req.tag.tag_code}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs font-medium px-2 py-0.5 rounded bg-blue-100 text-blue-700">
|
<span className="text-xs font-medium px-2 py-0.5 rounded bg-blue-100 text-blue-700">
|
||||||
{versionDiff} version{versionDiff > 1 ? 's' : ''} behind
|
{versionDiff} {versionDiff > 1 ? t('needsRevalidation.versionsBehind') : t('needsRevalidation.versionBehind')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h4 className="font-medium text-gray-800 text-sm line-clamp-2 mb-2">
|
<h4 className="font-medium text-gray-800 text-sm line-clamp-2 mb-2">
|
||||||
@@ -717,8 +715,8 @@ export default function DashboardPage() {
|
|||||||
v{req.validation_version} → v{req.version}
|
v{req.validation_version} → v{req.version}
|
||||||
</span>
|
</span>
|
||||||
{req.validated_by && (
|
{req.validated_by && (
|
||||||
<span className="truncate max-w-[120px]" title={`Last validated by ${req.validated_by}`}>
|
<span className="truncate max-w-[120px]" title={`${t('needsRevalidation.lastValidatedBy')} ${req.validated_by}`}>
|
||||||
by {req.validated_by}
|
{t('needsRevalidation.by')} {req.validated_by}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -734,7 +732,7 @@ export default function DashboardPage() {
|
|||||||
onClick={handleNeedsRevalidationClick}
|
onClick={handleNeedsRevalidationClick}
|
||||||
className="text-sm text-blue-600 hover:text-blue-700 font-medium"
|
className="text-sm text-blue-600 hover:text-blue-700 font-medium"
|
||||||
>
|
>
|
||||||
+ {requirementsNeedingRevalidation.length - 6} more requirements need revalidation
|
+ {requirementsNeedingRevalidation.length - 6} {t('needsRevalidation.moreRequirements')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -751,9 +749,9 @@ export default function DashboardPage() {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-green-800">All Clear!</h3>
|
<h3 className="font-semibold text-green-800">{t('allClear.title')}</h3>
|
||||||
<p className="text-sm text-green-600">
|
<p className="text-sm text-green-600">
|
||||||
No requirements need attention. All validations are either approved or pending review.
|
{t('allClear.message')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -776,7 +774,7 @@ export default function DashboardPage() {
|
|||||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
|
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
||||||
<h2 className="text-xl font-semibold text-gray-800">Create New Project</h2>
|
<h2 className="text-xl font-semibold text-gray-800">{t('createProject.title')}</h2>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowCreateProjectModal(false)
|
setShowCreateProjectModal(false)
|
||||||
@@ -805,13 +803,13 @@ export default function DashboardPage() {
|
|||||||
{/* Project Name */}
|
{/* Project Name */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Project Name <span className="text-red-500">*</span>
|
{t('createProject.projectName')} <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={newProjectName}
|
value={newProjectName}
|
||||||
onChange={(e) => setNewProjectName(e.target.value)}
|
onChange={(e) => setNewProjectName(e.target.value)}
|
||||||
placeholder="Enter project name"
|
placeholder={t('createProject.projectNamePlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -820,12 +818,12 @@ export default function DashboardPage() {
|
|||||||
{/* Project Description */}
|
{/* Project Description */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Description
|
{t('createProject.description')}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={newProjectDesc}
|
value={newProjectDesc}
|
||||||
onChange={(e) => setNewProjectDesc(e.target.value)}
|
onChange={(e) => setNewProjectDesc(e.target.value)}
|
||||||
placeholder="Enter project description (optional)"
|
placeholder={t('createProject.descriptionPlaceholder')}
|
||||||
rows={3}
|
rows={3}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent resize-none"
|
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent resize-none"
|
||||||
/>
|
/>
|
||||||
@@ -845,14 +843,14 @@ export default function DashboardPage() {
|
|||||||
className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-100"
|
className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-100"
|
||||||
disabled={createProjectLoading}
|
disabled={createProjectLoading}
|
||||||
>
|
>
|
||||||
Cancel
|
{tCommon('cancel')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
disabled={createProjectLoading}
|
disabled={createProjectLoading}
|
||||||
>
|
>
|
||||||
{createProjectLoading ? 'Creating...' : 'Create Project'}
|
{createProjectLoading ? t('createProject.creating') : t('createProject.createButton')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -862,4 +860,3 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useAuth } from '@/hooks'
|
import { useAuth } from '@/hooks'
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const { isAuthenticated, login, isLoading } = useAuth()
|
const { isAuthenticated, login, isLoading } = useAuth()
|
||||||
|
const { t: tHome } = useTranslation('home')
|
||||||
|
const { t: tNavbar } = useTranslation('navbar')
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -32,10 +35,10 @@ export default function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl">
|
<h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl">
|
||||||
Requirements Periodic Table
|
{tHome('title')}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mx-auto mt-4 max-w-md text-lg text-gray-600">
|
<p className="mx-auto mt-4 max-w-md text-lg text-gray-600">
|
||||||
Manage and track your project requirements.
|
{tHome('subtitle')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
@@ -57,14 +60,14 @@ export default function HomePage() {
|
|||||||
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
|
d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Login
|
{tNavbar('login')}
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<a
|
<a
|
||||||
href="/dashboard"
|
href="/dashboard"
|
||||||
className="inline-flex items-center gap-2 rounded-lg bg-primary-600 px-8 py-3 text-lg font-semibold text-white shadow-lg transition-all hover:bg-primary-700 hover:shadow-xl focus:outline-none focus:ring-4 focus:ring-primary-300"
|
className="inline-flex items-center gap-2 rounded-lg bg-primary-600 px-8 py-3 text-lg font-semibold text-white shadow-lg transition-all hover:bg-primary-700 hover:shadow-xl focus:outline-none focus:ring-4 focus:ring-primary-300"
|
||||||
>
|
>
|
||||||
Go to Dashboard
|
{tHome('goToDashboard')}
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useAuth, useProject } from '@/hooks'
|
import { useAuth, useProject } from '@/hooks'
|
||||||
import { useParams, Link } from 'react-router-dom'
|
import { useParams, Link } from 'react-router-dom'
|
||||||
import { requirementService, validationService, relationshipService, commentService, tagService, priorityService, groupService, requirementStatusService } from '@/services'
|
import { requirementService, validationService, relationshipService, commentService, tagService, priorityService, groupService, requirementStatusService } from '@/services'
|
||||||
@@ -40,6 +41,8 @@ interface TimelineEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function RequirementDetailPage() {
|
export default function RequirementDetailPage() {
|
||||||
|
const { t } = useTranslation('requirementDetail')
|
||||||
|
const { t: tCommon } = useTranslation('common')
|
||||||
const { user, logout, isAuditor } = useAuth()
|
const { user, logout, isAuditor } = useAuth()
|
||||||
const { currentProject } = useProject()
|
const { currentProject } = useProject()
|
||||||
const { id } = useParams<{ id: string }>()
|
const { id } = useParams<{ id: string }>()
|
||||||
@@ -719,7 +722,7 @@ export default function RequirementDetailPage() {
|
|||||||
<div className="min-h-screen bg-white flex items-center justify-center">
|
<div className="min-h-screen bg-white flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-teal-600 mx-auto"></div>
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-teal-600 mx-auto"></div>
|
||||||
<p className="mt-4 text-gray-600">Loading requirement...</p>
|
<p className="mt-4 text-gray-600">{t('loadingRequirement')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -730,10 +733,10 @@ export default function RequirementDetailPage() {
|
|||||||
<div className="min-h-screen bg-white flex items-center justify-center">
|
<div className="min-h-screen bg-white flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h2 className="text-2xl font-semibold text-gray-700 mb-4">
|
<h2 className="text-2xl font-semibold text-gray-700 mb-4">
|
||||||
{error || 'Requirement not found'}
|
{error || t('requirementNotFound')}
|
||||||
</h2>
|
</h2>
|
||||||
<Link to="/requirements" className="text-teal-600 hover:underline">
|
<Link to="/requirements" className="text-teal-600 hover:underline">
|
||||||
Back to Requirements
|
{t('backToRequirements')}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -741,12 +744,12 @@ export default function RequirementDetailPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tabs: { id: TabType; label: string }[] = [
|
const tabs: { id: TabType; label: string }[] = [
|
||||||
{ id: 'description', label: 'Description' },
|
{ id: 'description', label: t('tabs.description') },
|
||||||
{ id: 'relationships', label: 'Relationships' },
|
{ id: 'relationships', label: t('tabs.relationships') },
|
||||||
{ id: 'acceptance-criteria', label: 'Acceptance Criteria' },
|
{ id: 'acceptance-criteria', label: t('tabs.acceptanceCriteria') },
|
||||||
{ id: 'shared-comments', label: 'Shared Comments' },
|
{ id: 'shared-comments', label: t('tabs.sharedComments') },
|
||||||
{ id: 'validate', label: 'Validate' },
|
{ id: 'validate', label: t('tabs.validate') },
|
||||||
{ id: 'history', label: 'History' },
|
{ id: 'history', label: t('tabs.history') },
|
||||||
]
|
]
|
||||||
|
|
||||||
// Get display values from the requirement data
|
// Get display values from the requirement data
|
||||||
@@ -759,7 +762,7 @@ export default function RequirementDetailPage() {
|
|||||||
case 'description':
|
case 'description':
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-bold text-gray-800 mb-2">Description</h3>
|
<h3 className="text-xl font-bold text-gray-800 mb-2">{t('description.title')}</h3>
|
||||||
|
|
||||||
{/* Draft indicator banner */}
|
{/* Draft indicator banner */}
|
||||||
{isDraftStatus && (
|
{isDraftStatus && (
|
||||||
@@ -767,18 +770,18 @@ export default function RequirementDetailPage() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-amber-600 text-lg">📝</span>
|
<span className="text-amber-600 text-lg">📝</span>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-semibold text-amber-800">Draft Requirement</p>
|
<p className="font-semibold text-amber-800">{t('draft.title')}</p>
|
||||||
<p className="text-sm text-amber-700">This requirement is still in draft status and is not finalized. It may be subject to changes.</p>
|
<p className="text-sm text-amber-700">{t('draft.message')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-sm text-gray-700 mb-2">
|
<p className="text-sm text-gray-700 mb-2">
|
||||||
<span className="font-semibold">Version:</span> {requirement.version}
|
<span className="font-semibold">{t('description.version')}</span> {requirement.version}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-700 mb-2">
|
<p className="text-sm text-gray-700 mb-2">
|
||||||
<span className="font-semibold">Status:</span>{' '}
|
<span className="font-semibold">{t('description.status')}</span>{' '}
|
||||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||||
isDraftStatus
|
isDraftStatus
|
||||||
? 'bg-amber-100 text-amber-800 border border-amber-300'
|
? 'bg-amber-100 text-amber-800 border border-amber-300'
|
||||||
@@ -788,7 +791,7 @@ export default function RequirementDetailPage() {
|
|||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-700 mb-2">
|
<p className="text-sm text-gray-700 mb-2">
|
||||||
<span className="font-semibold">Validation Status:</span>{' '}
|
<span className="font-semibold">{t('description.validationStatus')}</span>{' '}
|
||||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getValidationStatusStyle(validationStatus)}`}>
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getValidationStatusStyle(validationStatus)}`}>
|
||||||
{validationStatus}
|
{validationStatus}
|
||||||
</span>
|
</span>
|
||||||
@@ -802,7 +805,7 @@ export default function RequirementDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-700 mb-2">
|
<p className="text-sm text-gray-700 mb-2">
|
||||||
<span className="font-semibold">Author:</span>{' '}
|
<span className="font-semibold">{t('description.author')}</span>{' '}
|
||||||
{requirement.author_username ? (
|
{requirement.author_username ? (
|
||||||
<span className="text-gray-700">{requirement.author_username}</span>
|
<span className="text-gray-700">{requirement.author_username}</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -811,22 +814,22 @@ export default function RequirementDetailPage() {
|
|||||||
</p>
|
</p>
|
||||||
{requirement.last_editor_username && (
|
{requirement.last_editor_username && (
|
||||||
<p className="text-sm text-gray-700 mb-2">
|
<p className="text-sm text-gray-700 mb-2">
|
||||||
<span className="font-semibold">Last Edited By:</span>{' '}
|
<span className="font-semibold">{t('description.lastEditedBy')}</span>{' '}
|
||||||
<span className="text-gray-700">{requirement.last_editor_username}</span>
|
<span className="text-gray-700">{requirement.last_editor_username}</span>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{requirement.created_at && (
|
{requirement.created_at && (
|
||||||
<p className="text-sm text-gray-700 mb-2">
|
<p className="text-sm text-gray-700 mb-2">
|
||||||
<span className="font-semibold">Created:</span> {new Date(requirement.created_at).toLocaleDateString()}
|
<span className="font-semibold">{t('description.created')}</span> {new Date(requirement.created_at).toLocaleDateString()}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{requirement.updated_at && (
|
{requirement.updated_at && (
|
||||||
<p className="text-sm text-gray-700 mb-4">
|
<p className="text-sm text-gray-700 mb-4">
|
||||||
<span className="font-semibold">Last Updated:</span> {new Date(requirement.updated_at).toLocaleDateString()}
|
<span className="font-semibold">{t('description.lastUpdated')}</span> {new Date(requirement.updated_at).toLocaleDateString()}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="bg-teal-50 border border-gray-300 rounded min-h-[300px] p-4">
|
<div className="bg-teal-50 border border-gray-300 rounded min-h-[300px] p-4">
|
||||||
<p className="text-gray-700">{requirement.req_desc || 'No description provided.'}</p>
|
<p className="text-gray-700">{requirement.req_desc || t('description.noDescription')}</p>
|
||||||
</div>
|
</div>
|
||||||
{/* Hide Edit button for auditors */}
|
{/* Hide Edit button for auditors */}
|
||||||
{!isAuditor && (
|
{!isAuditor && (
|
||||||
@@ -835,7 +838,7 @@ export default function RequirementDetailPage() {
|
|||||||
onClick={openEditModal}
|
onClick={openEditModal}
|
||||||
className="px-4 py-1.5 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50"
|
className="px-4 py-1.5 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50"
|
||||||
>
|
>
|
||||||
Edit
|
{t('description.editButton')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -846,43 +849,43 @@ export default function RequirementDetailPage() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h3 className="text-xl font-bold text-gray-800">Relationships</h3>
|
<h3 className="text-xl font-bold text-gray-800">{t('relationships.title')}</h3>
|
||||||
{!isAuditor && (
|
{!isAuditor && (
|
||||||
<button
|
<button
|
||||||
onClick={openAddRelationshipModal}
|
onClick={openAddRelationshipModal}
|
||||||
disabled={relationshipTypes.length === 0}
|
disabled={relationshipTypes.length === 0}
|
||||||
className="px-4 py-1.5 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="px-4 py-1.5 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
title={relationshipTypes.length === 0 ? 'No relationship types defined for this project. Contact an admin to set them up.' : ''}
|
title={relationshipTypes.length === 0 ? t('relationships.noTypesWarning') : ''}
|
||||||
>
|
>
|
||||||
Add Relationship
|
{t('relationships.addButton')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{relationshipTypes.length === 0 && !relationshipsLoading && (
|
{relationshipTypes.length === 0 && !relationshipsLoading && (
|
||||||
<div className="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded text-sm text-yellow-800">
|
<div className="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded text-sm text-yellow-800">
|
||||||
No relationship types have been defined for this project. Contact an administrator to set up relationship types.
|
{t('relationships.noTypesWarning')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{relationshipsLoading ? (
|
{relationshipsLoading ? (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-teal-600 mx-auto"></div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-teal-600 mx-auto"></div>
|
||||||
<p className="mt-2 text-gray-500 text-sm">Loading relationships...</p>
|
<p className="mt-2 text-gray-500 text-sm">{t('relationships.loadingRelationships')}</p>
|
||||||
</div>
|
</div>
|
||||||
) : relationshipLinks.length === 0 ? (
|
) : relationshipLinks.length === 0 ? (
|
||||||
<p className="text-gray-500 text-center py-8">No relationships defined yet.</p>
|
<p className="text-gray-500 text-center py-8">{t('relationships.noRelationships')}</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full border border-gray-200 rounded">
|
<table className="w-full border border-gray-200 rounded">
|
||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Direction</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('relationships.tableHeaders.direction')}</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Type</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('relationships.tableHeaders.type')}</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Linked Requirement</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('relationships.tableHeaders.linkedRequirement')}</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created By</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('relationships.tableHeaders.createdBy')}</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('relationships.tableHeaders.date')}</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('relationships.tableHeaders.actions')}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
@@ -894,7 +897,7 @@ export default function RequirementDetailPage() {
|
|||||||
? 'bg-blue-100 text-blue-800'
|
? 'bg-blue-100 text-blue-800'
|
||||||
: 'bg-purple-100 text-purple-800'
|
: 'bg-purple-100 text-purple-800'
|
||||||
}`}>
|
}`}>
|
||||||
{link.direction === 'outgoing' ? '→ Outgoing' : '← Incoming'}
|
{link.direction === 'outgoing' ? t('relationships.outgoing') : t('relationships.incoming')}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-sm text-gray-700">
|
<td className="px-4 py-3 text-sm text-gray-700">
|
||||||
@@ -944,12 +947,12 @@ export default function RequirementDetailPage() {
|
|||||||
case 'acceptance-criteria':
|
case 'acceptance-criteria':
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-bold text-gray-800 mb-4">Acceptance Criteria</h3>
|
<h3 className="text-xl font-bold text-gray-800 mb-4">{t('acceptanceCriteria.title')}</h3>
|
||||||
<p className="text-gray-500">No acceptance criteria defined yet.</p>
|
<p className="text-gray-500">{t('acceptanceCriteria.noCriteria')}</p>
|
||||||
{!isAuditor && (
|
{!isAuditor && (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<button className="px-4 py-1.5 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50">
|
<button className="px-4 py-1.5 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50">
|
||||||
Add Criterion
|
{t('acceptanceCriteria.addButton')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -959,14 +962,14 @@ export default function RequirementDetailPage() {
|
|||||||
case 'shared-comments':
|
case 'shared-comments':
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-bold text-gray-800 mb-4">Shared Comments</h3>
|
<h3 className="text-xl font-bold text-gray-800 mb-4">{t('comments.title')}</h3>
|
||||||
|
|
||||||
{/* New Comment Form */}
|
{/* New Comment Form */}
|
||||||
<div className="mb-6 p-4 border border-gray-200 rounded bg-gray-50">
|
<div className="mb-6 p-4 border border-gray-200 rounded bg-gray-50">
|
||||||
<textarea
|
<textarea
|
||||||
value={newCommentText}
|
value={newCommentText}
|
||||||
onChange={(e) => setNewCommentText(e.target.value)}
|
onChange={(e) => setNewCommentText(e.target.value)}
|
||||||
placeholder="Add a comment..."
|
placeholder={t('comments.placeholder')}
|
||||||
className="w-full p-3 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 resize-none"
|
className="w-full p-3 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 resize-none"
|
||||||
rows={3}
|
rows={3}
|
||||||
disabled={postingComment}
|
disabled={postingComment}
|
||||||
@@ -977,7 +980,7 @@ export default function RequirementDetailPage() {
|
|||||||
disabled={postingComment || !newCommentText.trim()}
|
disabled={postingComment || !newCommentText.trim()}
|
||||||
className="px-4 py-1.5 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="px-4 py-1.5 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{postingComment ? 'Posting...' : 'Post Comment'}
|
{postingComment ? t('comments.posting') : t('comments.postButton')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -986,10 +989,10 @@ export default function RequirementDetailPage() {
|
|||||||
{commentsLoading ? (
|
{commentsLoading ? (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-teal-600 mx-auto"></div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-teal-600 mx-auto"></div>
|
||||||
<p className="mt-2 text-gray-500 text-sm">Loading comments...</p>
|
<p className="mt-2 text-gray-500 text-sm">{t('comments.loadingComments')}</p>
|
||||||
</div>
|
</div>
|
||||||
) : comments.length === 0 ? (
|
) : comments.length === 0 ? (
|
||||||
<p className="text-gray-500 text-center py-8">No comments yet. Be the first to comment!</p>
|
<p className="text-gray-500 text-center py-8">{t('comments.noComments')}</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{comments.map((comment) => (
|
{comments.map((comment) => (
|
||||||
@@ -1038,7 +1041,7 @@ export default function RequirementDetailPage() {
|
|||||||
}}
|
}}
|
||||||
className="mt-2 text-xs text-teal-600 hover:text-teal-700 font-medium"
|
className="mt-2 text-xs text-teal-600 hover:text-teal-700 font-medium"
|
||||||
>
|
>
|
||||||
{replyingToCommentId === comment.id ? 'Cancel Reply' : 'Reply'}
|
{replyingToCommentId === comment.id ? t('comments.cancelReply') : t('comments.reply')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Reply Form */}
|
{/* Reply Form */}
|
||||||
@@ -1047,7 +1050,7 @@ export default function RequirementDetailPage() {
|
|||||||
<textarea
|
<textarea
|
||||||
value={replyText}
|
value={replyText}
|
||||||
onChange={(e) => setReplyText(e.target.value)}
|
onChange={(e) => setReplyText(e.target.value)}
|
||||||
placeholder="Write a reply..."
|
placeholder={t('comments.replyPlaceholder')}
|
||||||
className="w-full p-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 resize-none"
|
className="w-full p-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 resize-none"
|
||||||
rows={2}
|
rows={2}
|
||||||
disabled={postingReply}
|
disabled={postingReply}
|
||||||
@@ -1058,7 +1061,7 @@ export default function RequirementDetailPage() {
|
|||||||
disabled={postingReply || !replyText.trim()}
|
disabled={postingReply || !replyText.trim()}
|
||||||
className="px-3 py-1 bg-teal-600 text-white rounded text-xs font-medium hover:bg-teal-700 disabled:opacity-50"
|
className="px-3 py-1 bg-teal-600 text-white rounded text-xs font-medium hover:bg-teal-700 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{postingReply ? 'Posting...' : 'Post Reply'}
|
{postingReply ? t('comments.posting') : t('comments.postReply')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1117,25 +1120,25 @@ export default function RequirementDetailPage() {
|
|||||||
case 'validate':
|
case 'validate':
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-bold text-gray-800 mb-4">Validate Requirement</h3>
|
<h3 className="text-xl font-bold text-gray-800 mb-4">{t('validate.title')}</h3>
|
||||||
|
|
||||||
{/* Current Status */}
|
{/* Current Status */}
|
||||||
<div className="mb-6 p-4 bg-gray-50 border border-gray-200 rounded">
|
<div className="mb-6 p-4 bg-gray-50 border border-gray-200 rounded">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600">Current Status:</p>
|
<p className="text-sm text-gray-600">{t('validate.currentStatus')}</p>
|
||||||
<span className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${getValidationStatusStyle(validationStatus)}`}>
|
<span className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${getValidationStatusStyle(validationStatus)}`}>
|
||||||
{validationStatus}
|
{validationStatus}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="text-sm text-gray-600">Requirement Version:</p>
|
<p className="text-sm text-gray-600">{t('validate.requirementVersion')}</p>
|
||||||
<span className="text-lg font-semibold text-gray-800">{requirement.version}</span>
|
<span className="text-lg font-semibold text-gray-800">{requirement.version}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{requirement.validated_by && (
|
{requirement.validated_by && (
|
||||||
<p className="text-sm text-gray-500 mt-2">
|
<p className="text-sm text-gray-500 mt-2">
|
||||||
Last validated by {requirement.validated_by}
|
{t('validate.lastValidatedBy')} {requirement.validated_by}
|
||||||
{requirement.validated_at && ` on ${new Date(requirement.validated_at).toLocaleDateString()}`}
|
{requirement.validated_at && ` on ${new Date(requirement.validated_at).toLocaleDateString()}`}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -1144,7 +1147,7 @@ export default function RequirementDetailPage() {
|
|||||||
<p className="text-sm text-orange-800 flex items-center gap-2">
|
<p className="text-sm text-orange-800 flex items-center gap-2">
|
||||||
<span>⚠️</span>
|
<span>⚠️</span>
|
||||||
<span>
|
<span>
|
||||||
This requirement was modified after the last validation (validated at version {requirement.validation_version}, current version {requirement.version}).
|
{t('validate.staleWarning', { validationVersion: requirement.validation_version, currentVersion: requirement.version })}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1154,7 +1157,7 @@ export default function RequirementDetailPage() {
|
|||||||
{/* Validation Form - Only for auditors */}
|
{/* Validation Form - Only for auditors */}
|
||||||
{isAuditor && (
|
{isAuditor && (
|
||||||
<div className="mb-6 p-4 border border-gray-300 rounded">
|
<div className="mb-6 p-4 border border-gray-300 rounded">
|
||||||
<h4 className="font-semibold text-gray-800 mb-4">Submit Validation</h4>
|
<h4 className="font-semibold text-gray-800 mb-4">{t('validate.submitTitle')}</h4>
|
||||||
|
|
||||||
{validationError && (
|
{validationError && (
|
||||||
<div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded text-sm">
|
<div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded text-sm">
|
||||||
@@ -1166,7 +1169,7 @@ export default function RequirementDetailPage() {
|
|||||||
{/* Status Selection */}
|
{/* Status Selection */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Validation Status <span className="text-red-500">*</span>
|
{t('validate.statusLabel')} <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={selectedStatusId}
|
value={selectedStatusId}
|
||||||
@@ -1174,7 +1177,7 @@ export default function RequirementDetailPage() {
|
|||||||
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
||||||
disabled={validationLoading}
|
disabled={validationLoading}
|
||||||
>
|
>
|
||||||
<option value="">Select a status...</option>
|
<option value="">{t('validate.selectStatus')}</option>
|
||||||
{validationStatuses.map((status) => (
|
{validationStatuses.map((status) => (
|
||||||
<option key={status.id} value={status.id}>
|
<option key={status.id} value={status.id}>
|
||||||
{status.status_name}
|
{status.status_name}
|
||||||
@@ -1186,12 +1189,12 @@ export default function RequirementDetailPage() {
|
|||||||
{/* Comment */}
|
{/* Comment */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Comment
|
{t('validate.commentLabel')}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={validationComment}
|
value={validationComment}
|
||||||
onChange={(e) => setValidationComment(e.target.value)}
|
onChange={(e) => setValidationComment(e.target.value)}
|
||||||
placeholder="Add a comment explaining your decision (optional but recommended)"
|
placeholder={t('validate.commentPlaceholder')}
|
||||||
rows={4}
|
rows={4}
|
||||||
className="w-full p-3 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 resize-none"
|
className="w-full p-3 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 resize-none"
|
||||||
disabled={validationLoading}
|
disabled={validationLoading}
|
||||||
@@ -1204,7 +1207,7 @@ export default function RequirementDetailPage() {
|
|||||||
disabled={validationLoading || !selectedStatusId}
|
disabled={validationLoading || !selectedStatusId}
|
||||||
className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{validationLoading ? 'Submitting...' : 'Submit Validation'}
|
{validationLoading ? t('validate.submitting') : t('validate.submitButton')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1212,25 +1215,25 @@ export default function RequirementDetailPage() {
|
|||||||
|
|
||||||
{/* Validation History */}
|
{/* Validation History */}
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-semibold text-gray-800 mb-4">Validation History</h4>
|
<h4 className="font-semibold text-gray-800 mb-4">{t('validate.historyTitle')}</h4>
|
||||||
|
|
||||||
{historyLoading ? (
|
{historyLoading ? (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-teal-600 mx-auto"></div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-teal-600 mx-auto"></div>
|
||||||
<p className="mt-2 text-gray-500 text-sm">Loading history...</p>
|
<p className="mt-2 text-gray-500 text-sm">{t('validate.loadingHistory')}</p>
|
||||||
</div>
|
</div>
|
||||||
) : validationHistory.length === 0 ? (
|
) : validationHistory.length === 0 ? (
|
||||||
<p className="text-gray-500 text-center py-8">No validation history yet.</p>
|
<p className="text-gray-500 text-center py-8">{t('validate.noHistory')}</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full border border-gray-200 rounded">
|
<table className="w-full border border-gray-200 rounded">
|
||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('validate.tableHeaders.date')}</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('validate.tableHeaders.status')}</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Version</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('validate.tableHeaders.version')}</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Validator</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('validate.tableHeaders.validator')}</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Comment</th>
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('validate.tableHeaders.comment')}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
@@ -1252,7 +1255,7 @@ export default function RequirementDetailPage() {
|
|||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
v{validation.req_version_snapshot}
|
v{validation.req_version_snapshot}
|
||||||
{isStale && (
|
{isStale && (
|
||||||
<span className="text-orange-600" title="Requirement was modified after this validation">
|
<span className="text-orange-600" title={t('validate.staleWarning', { validationVersion: validation.req_version_snapshot, currentVersion: requirement.version })}>
|
||||||
⚠️
|
⚠️
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -1262,7 +1265,7 @@ export default function RequirementDetailPage() {
|
|||||||
{validation.validator_username}
|
{validation.validator_username}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-sm text-gray-700">
|
<td className="px-4 py-3 text-sm text-gray-700">
|
||||||
{validation.comment || <span className="text-gray-400 italic">No comment</span>}
|
{validation.comment || <span className="text-gray-400 italic">{t('validate.noComment')}</span>}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
@@ -1290,11 +1293,11 @@ export default function RequirementDetailPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-bold text-gray-800 mb-4">Version History</h3>
|
<h3 className="text-xl font-bold text-gray-800 mb-4">{t('history.title')}</h3>
|
||||||
|
|
||||||
{/* Author info */}
|
{/* Author info */}
|
||||||
<div className="mb-4 p-3 bg-gray-50 border border-gray-200 rounded text-sm">
|
<div className="mb-4 p-3 bg-gray-50 border border-gray-200 rounded text-sm">
|
||||||
<span className="font-medium text-gray-700">Original Author:</span>{' '}
|
<span className="font-medium text-gray-700">{t('history.originalAuthor')}</span>{' '}
|
||||||
{requirement.author_username ? (
|
{requirement.author_username ? (
|
||||||
<span className="text-gray-800">{requirement.author_username}</span>
|
<span className="text-gray-800">{requirement.author_username}</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -1310,10 +1313,10 @@ export default function RequirementDetailPage() {
|
|||||||
{reqHistoryLoading ? (
|
{reqHistoryLoading ? (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-teal-600 mx-auto"></div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-teal-600 mx-auto"></div>
|
||||||
<p className="mt-2 text-gray-500 text-sm">Loading history...</p>
|
<p className="mt-2 text-gray-500 text-sm">{t('history.loadingHistory')}</p>
|
||||||
</div>
|
</div>
|
||||||
) : timelineEvents.length === 0 ? (
|
) : timelineEvents.length === 0 ? (
|
||||||
<p className="text-gray-500 text-center py-8">No history yet. History is recorded when the requirement is edited or relationships change.</p>
|
<p className="text-gray-500 text-center py-8">{t('history.noHistory')}</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{timelineEvents.map((event) => {
|
{timelineEvents.map((event) => {
|
||||||
@@ -1346,7 +1349,7 @@ export default function RequirementDetailPage() {
|
|||||||
<span className="font-semibold text-gray-800">
|
<span className="font-semibold text-gray-800">
|
||||||
v{oldItem.version} → v{newItem ? newItem.version : requirement.version}
|
v{oldItem.version} → v{newItem ? newItem.version : requirement.version}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-gray-600">Requirement edited</span>
|
<span className="text-sm text-gray-600">{t('history.requirementEdited')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4 text-sm text-gray-500">
|
<div className="flex items-center gap-4 text-sm text-gray-500">
|
||||||
{oldItem.edited_by_username && (
|
{oldItem.edited_by_username && (
|
||||||
@@ -1365,7 +1368,7 @@ export default function RequirementDetailPage() {
|
|||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className="px-4 py-3 border-t border-gray-200 bg-white space-y-4">
|
<div className="px-4 py-3 border-t border-gray-200 bg-white space-y-4">
|
||||||
{!hasAnyChange && (
|
{!hasAnyChange && (
|
||||||
<p className="text-gray-500 italic text-sm">No visible changes detected (may be a group-only change).</p>
|
<p className="text-gray-500 italic text-sm">{t('history.noChangesDetected')}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Name diff */}
|
{/* Name diff */}
|
||||||
@@ -1463,7 +1466,7 @@ export default function RequirementDetailPage() {
|
|||||||
}}
|
}}
|
||||||
className="mt-2 text-sm text-teal-600 hover:underline"
|
className="mt-2 text-sm text-teal-600 hover:underline"
|
||||||
>
|
>
|
||||||
{showFullDescDiff === event.id ? 'Show less' : 'Show full diff'}
|
{showFullDescDiff === event.id ? t('history.showLess') : t('history.showFullDiff')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1481,7 +1484,7 @@ export default function RequirementDetailPage() {
|
|||||||
<div className="flex items-center justify-between px-4 py-3 bg-green-50 border-l-4 border-green-400">
|
<div className="flex items-center justify-between px-4 py-3 bg-green-50 border-l-4 border-green-400">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-green-600 text-lg">🔗</span>
|
<span className="text-green-600 text-lg">🔗</span>
|
||||||
<span className="font-medium text-gray-800">Link Created</span>
|
<span className="font-medium text-gray-800">{t('history.linkCreated')}</span>
|
||||||
<span className="text-sm text-gray-600">
|
<span className="text-sm text-gray-600">
|
||||||
{link.isSource ? (
|
{link.isSource ? (
|
||||||
<>
|
<>
|
||||||
@@ -1528,7 +1531,7 @@ export default function RequirementDetailPage() {
|
|||||||
<div className="flex items-center justify-between px-4 py-3 bg-red-50 border-l-4 border-red-400">
|
<div className="flex items-center justify-between px-4 py-3 bg-red-50 border-l-4 border-red-400">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-red-600 text-lg">🔗</span>
|
<span className="text-red-600 text-lg">🔗</span>
|
||||||
<span className="font-medium text-gray-800">Link Removed</span>
|
<span className="font-medium text-gray-800">{t('history.linkRemoved')}</span>
|
||||||
<span className="text-sm text-gray-600">
|
<span className="text-sm text-gray-600">
|
||||||
{link.isSource ? (
|
{link.isSource ? (
|
||||||
<>
|
<>
|
||||||
@@ -1572,7 +1575,7 @@ export default function RequirementDetailPage() {
|
|||||||
<div className="flex items-center justify-between px-4 py-3 bg-green-50 border-l-4 border-green-400">
|
<div className="flex items-center justify-between px-4 py-3 bg-green-50 border-l-4 border-green-400">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-green-600 text-lg">🏷️</span>
|
<span className="text-green-600 text-lg">🏷️</span>
|
||||||
<span className="font-medium text-gray-800">Group Added</span>
|
<span className="font-medium text-gray-800">{t('history.groupAdded')}</span>
|
||||||
<span className="text-sm text-gray-600">
|
<span className="text-sm text-gray-600">
|
||||||
{group.groupName ? (
|
{group.groupName ? (
|
||||||
<span
|
<span
|
||||||
@@ -1606,7 +1609,7 @@ export default function RequirementDetailPage() {
|
|||||||
<div className="flex items-center justify-between px-4 py-3 bg-orange-50 border-l-4 border-orange-400">
|
<div className="flex items-center justify-between px-4 py-3 bg-orange-50 border-l-4 border-orange-400">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-orange-600 text-lg">🏷️</span>
|
<span className="text-orange-600 text-lg">🏷️</span>
|
||||||
<span className="font-medium text-gray-800">Group Removed</span>
|
<span className="font-medium text-gray-800">{t('history.groupRemoved')}</span>
|
||||||
<span className="text-sm text-gray-600">
|
<span className="text-sm text-gray-600">
|
||||||
{group.groupName ? (
|
{group.groupName ? (
|
||||||
<span
|
<span
|
||||||
@@ -1650,7 +1653,7 @@ export default function RequirementDetailPage() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="py-6 text-center">
|
<header className="py-6 text-center">
|
||||||
<h1 className="text-3xl font-semibold text-teal-700">
|
<h1 className="text-3xl font-semibold text-teal-700">
|
||||||
Digital Twin Requirements Tool
|
{t('pageTitle')}
|
||||||
</h1>
|
</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -1659,22 +1662,13 @@ export default function RequirementDetailPage() {
|
|||||||
<div className="flex items-center justify-between max-w-7xl mx-auto">
|
<div className="flex items-center justify-between max-w-7xl mx-auto">
|
||||||
{/* Breadcrumb */}
|
{/* Breadcrumb */}
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
<Link to="/dashboard" className="text-gray-600 hover:underline">Projects</Link>
|
<Link to="/dashboard" className="text-gray-600 hover:underline">{tCommon('projects')}</Link>
|
||||||
<span className="mx-2 text-gray-400">»</span>
|
<span className="mx-2 text-gray-400">»</span>
|
||||||
<Link to="/dashboard" className="text-gray-600 hover:underline">{currentProject?.project_name || 'Project'}</Link>
|
<Link to="/dashboard" className="text-gray-600 hover:underline">{currentProject?.project_name || tCommon('project')}</Link>
|
||||||
<span className="mx-2 text-gray-400">»</span>
|
<span className="mx-2 text-gray-400">»</span>
|
||||||
<Link to="/requirements" className="text-gray-600 hover:underline">Search</Link>
|
<Link to="/requirements" className="text-gray-600 hover:underline">{t('breadcrumb.search')}</Link>
|
||||||
<span className="mx-2 text-gray-400">»</span>
|
<span className="mx-2 text-gray-400">»</span>
|
||||||
<span className="font-semibold text-gray-900">Details {tagCode}</span>
|
<span className="font-semibold text-gray-900">{t('breadcrumb.details')} {tagCode}</span>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Language Toggle */}
|
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
|
||||||
<span>English</span>
|
|
||||||
<div className="relative inline-flex h-5 w-10 items-center rounded-full bg-gray-300">
|
|
||||||
<span className="inline-block h-4 w-4 transform rounded-full bg-white shadow-sm translate-x-0.5" />
|
|
||||||
</div>
|
|
||||||
<span>Portuguese</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* User Info */}
|
{/* User Info */}
|
||||||
@@ -1684,15 +1678,15 @@ export default function RequirementDetailPage() {
|
|||||||
<path fillRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clipRule="evenodd" />
|
<path fillRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clipRule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="text-sm text-gray-700">
|
<span className="text-sm text-gray-700">
|
||||||
{user?.full_name || user?.preferred_username || 'User'}{' '}
|
{user?.full_name || user?.preferred_username || tCommon('user')}{' '}
|
||||||
<span className="text-gray-500">({user?.role || 'user'})</span>
|
<span className="text-gray-500">({user?.role || tCommon('user')})</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={logout}
|
onClick={logout}
|
||||||
className="px-3 py-1 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50"
|
className="px-3 py-1 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50"
|
||||||
>
|
>
|
||||||
Logout
|
{tCommon('logout')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1706,7 +1700,7 @@ export default function RequirementDetailPage() {
|
|||||||
{isDraftStatus && (
|
{isDraftStatus && (
|
||||||
<div className="mb-4 inline-flex items-center gap-2 px-4 py-2 bg-amber-100 border-2 border-dashed border-amber-400 rounded-lg">
|
<div className="mb-4 inline-flex items-center gap-2 px-4 py-2 bg-amber-100 border-2 border-dashed border-amber-400 rounded-lg">
|
||||||
<span className="text-amber-600 text-lg">📝</span>
|
<span className="text-amber-600 text-lg">📝</span>
|
||||||
<span className="text-amber-800 font-medium">Draft - Not Finalized</span>
|
<span className="text-amber-800 font-medium">{t('draft.badge')}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1788,7 +1782,7 @@ export default function RequirementDetailPage() {
|
|||||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-lg mx-4">
|
<div className="bg-white rounded-lg shadow-xl w-full max-w-lg mx-4">
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
||||||
<h2 className="text-xl font-semibold text-gray-800">Add Relationship</h2>
|
<h2 className="text-xl font-semibold text-gray-800">{t('addRelationshipModal.title')}</h2>
|
||||||
<button
|
<button
|
||||||
onClick={closeAddRelationshipModal}
|
onClick={closeAddRelationshipModal}
|
||||||
className="text-gray-400 hover:text-gray-600"
|
className="text-gray-400 hover:text-gray-600"
|
||||||
@@ -1808,7 +1802,7 @@ export default function RequirementDetailPage() {
|
|||||||
{/* Relationship Type Selection */}
|
{/* Relationship Type Selection */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Relationship Type <span className="text-red-500">*</span>
|
{t('addRelationshipModal.relationshipType')} <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={selectedRelationshipType}
|
value={selectedRelationshipType}
|
||||||
@@ -1816,7 +1810,7 @@ export default function RequirementDetailPage() {
|
|||||||
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
||||||
disabled={addLinkLoading}
|
disabled={addLinkLoading}
|
||||||
>
|
>
|
||||||
<option value="">Select a relationship type...</option>
|
<option value="">{t('addRelationshipModal.selectType')}</option>
|
||||||
{relationshipTypes.map((type) => (
|
{relationshipTypes.map((type) => (
|
||||||
<option key={type.id} value={type.id}>
|
<option key={type.id} value={type.id}>
|
||||||
{type.type_name}
|
{type.type_name}
|
||||||
@@ -1829,7 +1823,7 @@ export default function RequirementDetailPage() {
|
|||||||
{/* Target Requirement Search */}
|
{/* Target Requirement Search */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Target Requirement <span className="text-red-500">*</span>
|
{t('addRelationshipModal.targetRequirement')} <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
@@ -1839,7 +1833,7 @@ export default function RequirementDetailPage() {
|
|||||||
setTargetSearchQuery(e.target.value)
|
setTargetSearchQuery(e.target.value)
|
||||||
setSelectedTarget(null)
|
setSelectedTarget(null)
|
||||||
}}
|
}}
|
||||||
placeholder="Search by tag code or name..."
|
placeholder={t('addRelationshipModal.searchPlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
||||||
disabled={addLinkLoading}
|
disabled={addLinkLoading}
|
||||||
/>
|
/>
|
||||||
@@ -1868,7 +1862,7 @@ export default function RequirementDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
{selectedTarget && (
|
{selectedTarget && (
|
||||||
<p className="mt-1 text-sm text-green-600">
|
<p className="mt-1 text-sm text-green-600">
|
||||||
✓ Selected: {selectedTarget.tag_code} - {selectedTarget.req_name}
|
✓ {t('addRelationshipModal.selected')} {selectedTarget.tag_code} - {selectedTarget.req_name}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1882,7 +1876,7 @@ export default function RequirementDetailPage() {
|
|||||||
className="px-4 py-2 border border-gray-300 rounded text-sm text-gray-700 hover:bg-gray-100"
|
className="px-4 py-2 border border-gray-300 rounded text-sm text-gray-700 hover:bg-gray-100"
|
||||||
disabled={addLinkLoading}
|
disabled={addLinkLoading}
|
||||||
>
|
>
|
||||||
Cancel
|
{tCommon('cancel')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1890,7 +1884,7 @@ export default function RequirementDetailPage() {
|
|||||||
disabled={addLinkLoading || !selectedRelationshipType || !selectedTarget}
|
disabled={addLinkLoading || !selectedRelationshipType || !selectedTarget}
|
||||||
className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{addLinkLoading ? 'Creating...' : 'Create Relationship'}
|
{addLinkLoading ? tCommon('creating') : t('addRelationshipModal.createButton')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1903,7 +1897,7 @@ export default function RequirementDetailPage() {
|
|||||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl mx-4 max-h-[90vh] overflow-hidden flex flex-col">
|
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl mx-4 max-h-[90vh] overflow-hidden flex flex-col">
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
||||||
<h2 className="text-xl font-semibold text-gray-800">Edit Requirement</h2>
|
<h2 className="text-xl font-semibold text-gray-800">{t('editModal.title')}</h2>
|
||||||
<button
|
<button
|
||||||
onClick={closeEditModal}
|
onClick={closeEditModal}
|
||||||
className="text-gray-400 hover:text-gray-600"
|
className="text-gray-400 hover:text-gray-600"
|
||||||
@@ -1923,14 +1917,14 @@ export default function RequirementDetailPage() {
|
|||||||
{editOptionsLoading ? (
|
{editOptionsLoading ? (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-teal-600 mx-auto"></div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-teal-600 mx-auto"></div>
|
||||||
<p className="mt-2 text-gray-500 text-sm">Loading options...</p>
|
<p className="mt-2 text-gray-500 text-sm">{t('editModal.loadingOptions')}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Name */}
|
{/* Name */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Name <span className="text-red-500">*</span>
|
{t('editModal.name')} <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -1944,7 +1938,7 @@ export default function RequirementDetailPage() {
|
|||||||
{/* Tag */}
|
{/* Tag */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Tag <span className="text-red-500">*</span>
|
{t('editModal.tag')} <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={editTagId}
|
value={editTagId}
|
||||||
@@ -1952,7 +1946,7 @@ export default function RequirementDetailPage() {
|
|||||||
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
||||||
disabled={editLoading}
|
disabled={editLoading}
|
||||||
>
|
>
|
||||||
<option value="">Select a tag...</option>
|
<option value="">{t('editModal.selectTag')}</option>
|
||||||
{availableTags.map((tag) => (
|
{availableTags.map((tag) => (
|
||||||
<option key={tag.id} value={tag.id}>
|
<option key={tag.id} value={tag.id}>
|
||||||
{tag.tag_code} - {tag.tag_description}
|
{tag.tag_code} - {tag.tag_description}
|
||||||
@@ -1964,7 +1958,7 @@ export default function RequirementDetailPage() {
|
|||||||
{/* Priority */}
|
{/* Priority */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Priority
|
{t('editModal.priority')}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={editPriorityId}
|
value={editPriorityId}
|
||||||
@@ -1972,7 +1966,7 @@ export default function RequirementDetailPage() {
|
|||||||
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
||||||
disabled={editLoading}
|
disabled={editLoading}
|
||||||
>
|
>
|
||||||
<option value="">No priority</option>
|
<option value="">{t('editModal.noPriority')}</option>
|
||||||
{availablePriorities.map((priority) => (
|
{availablePriorities.map((priority) => (
|
||||||
<option key={priority.id} value={priority.id}>
|
<option key={priority.id} value={priority.id}>
|
||||||
{priority.priority_name}
|
{priority.priority_name}
|
||||||
@@ -1984,7 +1978,7 @@ export default function RequirementDetailPage() {
|
|||||||
{/* Status */}
|
{/* Status */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Status
|
{t('editModal.status')}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={editStatusId}
|
value={editStatusId}
|
||||||
@@ -1999,14 +1993,14 @@ export default function RequirementDetailPage() {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<p className="mt-1 text-xs text-gray-500">
|
<p className="mt-1 text-xs text-gray-500">
|
||||||
Draft requirements are not finalized and marked with a visual indicator.
|
{t('editModal.draftNote')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Description
|
{t('editModal.description')}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={editReqDesc}
|
value={editReqDesc}
|
||||||
@@ -2020,10 +2014,10 @@ export default function RequirementDetailPage() {
|
|||||||
{/* Groups */}
|
{/* Groups */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Groups
|
{t('editModal.groups')}
|
||||||
</label>
|
</label>
|
||||||
{availableGroups.length === 0 ? (
|
{availableGroups.length === 0 ? (
|
||||||
<p className="text-sm text-gray-500 italic">No groups available</p>
|
<p className="text-sm text-gray-500 italic">{t('editModal.noGroupsAvailable')}</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-2 gap-2 max-h-40 overflow-y-auto border border-gray-200 rounded p-3">
|
<div className="grid grid-cols-2 gap-2 max-h-40 overflow-y-auto border border-gray-200 rounded p-3">
|
||||||
{availableGroups.map((group) => (
|
{availableGroups.map((group) => (
|
||||||
@@ -2064,7 +2058,7 @@ export default function RequirementDetailPage() {
|
|||||||
className="px-4 py-2 border border-gray-300 rounded text-sm text-gray-700 hover:bg-gray-100"
|
className="px-4 py-2 border border-gray-300 rounded text-sm text-gray-700 hover:bg-gray-100"
|
||||||
disabled={editLoading}
|
disabled={editLoading}
|
||||||
>
|
>
|
||||||
Cancel
|
{tCommon('cancel')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -2072,7 +2066,7 @@ export default function RequirementDetailPage() {
|
|||||||
disabled={editLoading || editOptionsLoading || !editReqName.trim() || !editTagId}
|
disabled={editLoading || editOptionsLoading || !editReqName.trim() || !editTagId}
|
||||||
className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{editLoading ? 'Saving...' : 'Save Changes'}
|
{editLoading ? tCommon('saving') : tCommon('saveChanges')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2114,14 +2108,14 @@ export default function RequirementDetailPage() {
|
|||||||
className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-100"
|
className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-100"
|
||||||
disabled={deleteConfirmLoading}
|
disabled={deleteConfirmLoading}
|
||||||
>
|
>
|
||||||
Cancel
|
{tCommon('cancel')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleConfirmDelete}
|
onClick={handleConfirmDelete}
|
||||||
className="px-4 py-2 bg-red-600 text-white rounded text-sm font-medium hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="px-4 py-2 bg-red-600 text-white rounded text-sm font-medium hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
disabled={deleteConfirmLoading}
|
disabled={deleteConfirmLoading}
|
||||||
>
|
>
|
||||||
{deleteConfirmLoading ? 'Deleting...' : 'Delete'}
|
{deleteConfirmLoading ? tCommon('deleting') : tCommon('delete')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useAuth, useProject } from '@/hooks'
|
import { useAuth, useProject } from '@/hooks'
|
||||||
import { useSearchParams, Link, useNavigate, useLocation } from 'react-router-dom'
|
import { useSearchParams, Link, useNavigate, useLocation } from 'react-router-dom'
|
||||||
import { groupService, tagService, requirementService, priorityService } from '@/services'
|
import { groupService, tagService, requirementService, priorityService } from '@/services'
|
||||||
|
import { LanguageSelector } from '@/components'
|
||||||
import type { Group } from '@/services/groupService'
|
import type { Group } from '@/services/groupService'
|
||||||
import type { Tag } from '@/services/tagService'
|
import type { Tag } from '@/services/tagService'
|
||||||
import type { Priority } from '@/services/priorityService'
|
import type { Priority } from '@/services/priorityService'
|
||||||
@@ -30,6 +32,8 @@ const isDraftStatus = (statusCode: string | undefined): boolean => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function RequirementsPage() {
|
export default function RequirementsPage() {
|
||||||
|
const { t } = useTranslation('requirements')
|
||||||
|
const { t: tCommon } = useTranslation('common')
|
||||||
const { user, logout, isAuditor } = useAuth()
|
const { user, logout, isAuditor } = useAuth()
|
||||||
const { currentProject, isLoading: projectLoading } = useProject()
|
const { currentProject, isLoading: projectLoading } = useProject()
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
@@ -366,7 +370,7 @@ export default function RequirementsPage() {
|
|||||||
<div className="min-h-screen bg-white flex items-center justify-center">
|
<div className="min-h-screen bg-white flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-teal-600 mx-auto"></div>
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-teal-600 mx-auto"></div>
|
||||||
<p className="mt-4 text-gray-600">Loading requirements...</p>
|
<p className="mt-4 text-gray-600">{t('loadingRequirements')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -379,13 +383,13 @@ export default function RequirementsPage() {
|
|||||||
<svg className="w-16 h-16 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-16 h-16 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||||
</svg>
|
</svg>
|
||||||
<h2 className="text-xl font-semibold text-gray-700 mb-2">No Project Selected</h2>
|
<h2 className="text-xl font-semibold text-gray-700 mb-2">{t('noProjectSelected.title')}</h2>
|
||||||
<p className="text-gray-500 mb-4">Please select a project from the dashboard to view requirements.</p>
|
<p className="text-gray-500 mb-4">{t('noProjectSelected.message')}</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/dashboard')}
|
onClick={() => navigate('/dashboard')}
|
||||||
className="px-4 py-2 bg-teal-600 text-white rounded hover:bg-teal-700"
|
className="px-4 py-2 bg-teal-600 text-white rounded hover:bg-teal-700"
|
||||||
>
|
>
|
||||||
Go to Dashboard
|
{t('noProjectSelected.goToDashboard')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -401,7 +405,7 @@ export default function RequirementsPage() {
|
|||||||
onClick={() => window.location.reload()}
|
onClick={() => window.location.reload()}
|
||||||
className="px-4 py-2 bg-teal-600 text-white rounded hover:bg-teal-700"
|
className="px-4 py-2 bg-teal-600 text-white rounded hover:bg-teal-700"
|
||||||
>
|
>
|
||||||
Retry
|
{tCommon('retry')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -413,7 +417,7 @@ export default function RequirementsPage() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="py-6 text-center">
|
<header className="py-6 text-center">
|
||||||
<h1 className="text-3xl font-semibold text-teal-700">
|
<h1 className="text-3xl font-semibold text-teal-700">
|
||||||
Digital Twin Requirements Tool
|
{t('header.title')}
|
||||||
</h1>
|
</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -422,21 +426,15 @@ export default function RequirementsPage() {
|
|||||||
<div className="flex items-center justify-between max-w-7xl mx-auto">
|
<div className="flex items-center justify-between max-w-7xl mx-auto">
|
||||||
{/* Breadcrumb */}
|
{/* Breadcrumb */}
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
<Link to="/dashboard" className="text-gray-600 hover:underline">Projects</Link>
|
<Link to="/dashboard" className="text-gray-600 hover:underline">{t('breadcrumb.projects')}</Link>
|
||||||
<span className="mx-2 text-gray-400">»</span>
|
<span className="mx-2 text-gray-400">»</span>
|
||||||
<Link to="/dashboard" className="text-gray-600 hover:underline">{currentProject.project_name}</Link>
|
<Link to="/dashboard" className="text-gray-600 hover:underline">{currentProject.project_name}</Link>
|
||||||
<span className="mx-2 text-gray-400">»</span>
|
<span className="mx-2 text-gray-400">»</span>
|
||||||
<span className="font-semibold text-gray-900">Search Requirements</span>
|
<span className="font-semibold text-gray-900">{t('breadcrumb.searchRequirements')}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Language Toggle */}
|
{/* Language Selector */}
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
<LanguageSelector compact />
|
||||||
<span>English</span>
|
|
||||||
<div className="relative inline-flex h-5 w-10 items-center rounded-full bg-gray-300">
|
|
||||||
<span className="inline-block h-4 w-4 transform rounded-full bg-white shadow-sm translate-x-0.5" />
|
|
||||||
</div>
|
|
||||||
<span>Portuguese</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* User Info */}
|
{/* User Info */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -445,15 +443,15 @@ export default function RequirementsPage() {
|
|||||||
<path fillRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clipRule="evenodd" />
|
<path fillRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clipRule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="text-sm text-gray-700">
|
<span className="text-sm text-gray-700">
|
||||||
{user?.full_name || user?.preferred_username || 'User'}{' '}
|
{user?.full_name || user?.preferred_username || tCommon('user')}{' '}
|
||||||
<span className="text-gray-500">({user?.role || 'user'})</span>
|
<span className="text-gray-500">({user?.role || tCommon('user').toLowerCase()})</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={logout}
|
onClick={logout}
|
||||||
className="px-3 py-1 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50"
|
className="px-3 py-1 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50"
|
||||||
>
|
>
|
||||||
Logout
|
{tCommon('logout')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -470,7 +468,7 @@ export default function RequirementsPage() {
|
|||||||
onClick={openCreateModal}
|
onClick={openCreateModal}
|
||||||
className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-50"
|
className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-50"
|
||||||
>
|
>
|
||||||
New Requirement
|
{t('newRequirement')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={toggleDeletedPanel}
|
onClick={toggleDeletedPanel}
|
||||||
@@ -483,7 +481,7 @@ export default function RequirementsPage() {
|
|||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
</svg>
|
</svg>
|
||||||
Deleted
|
{t('deleted')}
|
||||||
{deletedRequirements.length > 0 && (
|
{deletedRequirements.length > 0 && (
|
||||||
<span className="bg-red-500 text-white text-xs px-1.5 py-0.5 rounded-full">
|
<span className="bg-red-500 text-white text-xs px-1.5 py-0.5 rounded-full">
|
||||||
{deletedRequirements.length}
|
{deletedRequirements.length}
|
||||||
@@ -505,7 +503,7 @@ export default function RequirementsPage() {
|
|||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
</svg>
|
</svg>
|
||||||
Deleted
|
{t('deleted')}
|
||||||
{deletedRequirements.length > 0 && (
|
{deletedRequirements.length > 0 && (
|
||||||
<span className="bg-red-500 text-white text-xs px-1.5 py-0.5 rounded-full">
|
<span className="bg-red-500 text-white text-xs px-1.5 py-0.5 rounded-full">
|
||||||
{deletedRequirements.length}
|
{deletedRequirements.length}
|
||||||
@@ -519,7 +517,7 @@ export default function RequirementsPage() {
|
|||||||
<div className="flex gap-2 mb-6">
|
<div className="flex gap-2 mb-6">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search for a requirement tag or title"
|
placeholder={t('searchPlaceholder')}
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="flex-1 px-4 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
className="flex-1 px-4 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
||||||
@@ -528,19 +526,19 @@ export default function RequirementsPage() {
|
|||||||
onClick={handleSearch}
|
onClick={handleSearch}
|
||||||
className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-50"
|
className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-50"
|
||||||
>
|
>
|
||||||
Search
|
{tCommon('search')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleClear}
|
onClick={handleClear}
|
||||||
className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-50"
|
className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-50"
|
||||||
>
|
>
|
||||||
Clear
|
{tCommon('clear')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filter Group */}
|
{/* Filter Group */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<p className="text-sm text-gray-600 mb-3">Filter Group</p>
|
<p className="text-sm text-gray-600 mb-3">{t('filters.filterGroup')}</p>
|
||||||
<div className="grid grid-cols-3 gap-x-8 gap-y-2">
|
<div className="grid grid-cols-3 gap-x-8 gap-y-2">
|
||||||
{groups.map((group) => (
|
{groups.map((group) => (
|
||||||
<label key={group.id} className="flex items-center gap-2 cursor-pointer">
|
<label key={group.id} className="flex items-center gap-2 cursor-pointer">
|
||||||
@@ -559,16 +557,16 @@ export default function RequirementsPage() {
|
|||||||
{/* Filter Validation Status */}
|
{/* Filter Validation Status */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<p className="text-sm text-gray-600">Filter Validation Status</p>
|
<p className="text-sm text-gray-600">{t('filters.filterValidationStatus')}</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{needsRevalidationFilter && (
|
{needsRevalidationFilter && (
|
||||||
<span className="text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded-full">
|
<span className="text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded-full">
|
||||||
Showing: Needs Revalidation
|
{t('filters.showingNeedsRevalidation')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{selectedValidationStatuses.includes('__NEEDS_ATTENTION__') && (
|
{selectedValidationStatuses.includes('__NEEDS_ATTENTION__') && (
|
||||||
<span className="text-xs bg-amber-100 text-amber-700 px-2 py-1 rounded-full">
|
<span className="text-xs bg-amber-100 text-amber-700 px-2 py-1 rounded-full">
|
||||||
Showing: Needs Attention
|
{t('filters.showingNeedsAttention')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -587,7 +585,7 @@ export default function RequirementsPage() {
|
|||||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||||
</svg>
|
</svg>
|
||||||
Needs Revalidation
|
{t('filters.needsRevalidation')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{['Approved', 'Denied', 'Partially Approved', 'Not Validated'].map((status) => {
|
{['Approved', 'Denied', 'Partially Approved', 'Not Validated'].map((status) => {
|
||||||
@@ -602,6 +600,12 @@ export default function RequirementsPage() {
|
|||||||
'Partially Approved': isSelected ? 'bg-yellow-100 border-yellow-500 text-yellow-800' : 'bg-white border-gray-300 text-gray-600 hover:border-yellow-400',
|
'Partially Approved': isSelected ? 'bg-yellow-100 border-yellow-500 text-yellow-800' : 'bg-white border-gray-300 text-gray-600 hover:border-yellow-400',
|
||||||
'Not Validated': isSelected ? 'bg-gray-200 border-gray-500 text-gray-800' : 'bg-white border-gray-300 text-gray-600 hover:border-gray-400',
|
'Not Validated': isSelected ? 'bg-gray-200 border-gray-500 text-gray-800' : 'bg-white border-gray-300 text-gray-600 hover:border-gray-400',
|
||||||
}
|
}
|
||||||
|
const statusTranslations: Record<string, string> = {
|
||||||
|
'Approved': t('filters.statuses.approved'),
|
||||||
|
'Denied': t('filters.statuses.denied'),
|
||||||
|
'Partially Approved': t('filters.statuses.partiallyApproved'),
|
||||||
|
'Not Validated': t('filters.statuses.notValidated'),
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={status}
|
key={status}
|
||||||
@@ -615,7 +619,7 @@ export default function RequirementsPage() {
|
|||||||
}}
|
}}
|
||||||
className={`px-3 py-1.5 rounded-full text-sm font-medium border-2 transition-colors ${statusStyles[status]}`}
|
className={`px-3 py-1.5 rounded-full text-sm font-medium border-2 transition-colors ${statusStyles[status]}`}
|
||||||
>
|
>
|
||||||
{status}
|
{statusTranslations[status]}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -625,18 +629,18 @@ export default function RequirementsPage() {
|
|||||||
{/* Order By */}
|
{/* Order By */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm text-gray-600">Order by:</span>
|
<span className="text-sm text-gray-600">{t('orderBy.label')}:</span>
|
||||||
<select
|
<select
|
||||||
value={orderBy}
|
value={orderBy}
|
||||||
onChange={(e) => setOrderBy(e.target.value as 'Date' | 'Priority' | 'Name')}
|
onChange={(e) => setOrderBy(e.target.value as 'Date' | 'Priority' | 'Name')}
|
||||||
className="px-3 py-1 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500"
|
className="px-3 py-1 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500"
|
||||||
>
|
>
|
||||||
<option value="Date">Date</option>
|
<option value="Date">{t('orderBy.date')}</option>
|
||||||
<option value="Priority">Priority</option>
|
<option value="Priority">{t('orderBy.priority')}</option>
|
||||||
<option value="Name">Name</option>
|
<option value="Name">{t('orderBy.name')}</option>
|
||||||
</select>
|
</select>
|
||||||
<button className="px-4 py-1 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-50">
|
<button className="px-4 py-1 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-50">
|
||||||
Filter
|
{tCommon('filter')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -644,12 +648,12 @@ export default function RequirementsPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setShowCaptionModal(true)}
|
onClick={() => setShowCaptionModal(true)}
|
||||||
className="flex items-center gap-2 px-3 py-1.5 border border-teal-500 rounded text-sm font-medium text-teal-700 hover:bg-teal-50 transition-colors"
|
className="flex items-center gap-2 px-3 py-1.5 border border-teal-500 rounded text-sm font-medium text-teal-700 hover:bg-teal-50 transition-colors"
|
||||||
title="View tag captions"
|
title={t('caption.viewCaptions')}
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
Caption
|
{t('caption.title')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -678,9 +682,9 @@ export default function RequirementsPage() {
|
|||||||
{isDraft && (
|
{isDraft && (
|
||||||
<span
|
<span
|
||||||
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800 border border-amber-300 flex-shrink-0"
|
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800 border border-amber-300 flex-shrink-0"
|
||||||
title="This requirement is still in draft and not finalized"
|
title={t('requirement.draftTooltip')}
|
||||||
>
|
>
|
||||||
📝 Draft
|
📝 {t('requirement.draft')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span
|
<span
|
||||||
@@ -711,13 +715,13 @@ export default function RequirementsPage() {
|
|||||||
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-600"
|
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-600"
|
||||||
title={req.groups.slice(2).map(g => g.group_name).join(', ')}
|
title={req.groups.slice(2).map(g => g.group_name).join(', ')}
|
||||||
>
|
>
|
||||||
+{req.groups.length - 2} more
|
+{req.groups.length - 2} {t('requirement.more')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-500">
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-500">
|
||||||
No groups
|
{t('requirement.noGroups')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -730,22 +734,22 @@ export default function RequirementsPage() {
|
|||||||
{validationStatus}
|
{validationStatus}
|
||||||
</span>
|
</span>
|
||||||
{isStale && (
|
{isStale && (
|
||||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-800" title="Requirement was modified after validation">
|
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-800" title={t('requirement.staleTooltip')}>
|
||||||
⚠ Stale
|
⚠ {t('requirement.stale')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{req.validated_by && (
|
{req.validated_by && (
|
||||||
<p className="text-xs text-gray-500 mt-1 text-center truncate" title={req.validated_by}>
|
<p className="text-xs text-gray-500 mt-1 text-center truncate" title={req.validated_by}>
|
||||||
by {req.validated_by}
|
{t('requirement.by')} {req.validated_by}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Priority and Version */}
|
{/* Priority and Version */}
|
||||||
<div className="w-[120px] lg:w-[140px] flex-shrink-0 px-3 py-3">
|
<div className="w-[120px] lg:w-[140px] flex-shrink-0 px-3 py-3">
|
||||||
<p className="text-sm text-gray-700 whitespace-nowrap">Priority: {priorityName}</p>
|
<p className="text-sm text-gray-700 whitespace-nowrap">{t('requirement.priority')}: {priorityName}</p>
|
||||||
<p className="text-sm text-gray-600">Version: {req.version}</p>
|
<p className="text-sm text-gray-600">{t('requirement.version')}: {req.version}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Spacer to push buttons to the right */}
|
{/* Spacer to push buttons to the right */}
|
||||||
@@ -757,14 +761,14 @@ export default function RequirementsPage() {
|
|||||||
onClick={() => handleDetails(req.id)}
|
onClick={() => handleDetails(req.id)}
|
||||||
className="px-3 py-1.5 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50 whitespace-nowrap"
|
className="px-3 py-1.5 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50 whitespace-nowrap"
|
||||||
>
|
>
|
||||||
Details
|
{tCommon('details')}
|
||||||
</button>
|
</button>
|
||||||
{!isAuditor && (
|
{!isAuditor && (
|
||||||
<button
|
<button
|
||||||
onClick={() => openDeleteModal(req.id, req.req_name)}
|
onClick={() => openDeleteModal(req.id, req.req_name)}
|
||||||
className="px-3 py-1.5 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50 whitespace-nowrap"
|
className="px-3 py-1.5 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50 whitespace-nowrap"
|
||||||
>
|
>
|
||||||
Remove
|
{tCommon('remove')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -774,7 +778,7 @@ export default function RequirementsPage() {
|
|||||||
|
|
||||||
{sortedRequirements.length === 0 && (
|
{sortedRequirements.length === 0 && (
|
||||||
<div className="text-center py-12 text-gray-500">
|
<div className="text-center py-12 text-gray-500">
|
||||||
No requirements found matching your criteria.
|
{t('noRequirementsFound')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -790,7 +794,7 @@ export default function RequirementsPage() {
|
|||||||
<svg className="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
</svg>
|
</svg>
|
||||||
<h2 className="text-lg font-semibold text-gray-800">Deleted Requirements</h2>
|
<h2 className="text-lg font-semibold text-gray-800">{t('deletedPanel.title')}</h2>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowDeletedPanel(false)}
|
onClick={() => setShowDeletedPanel(false)}
|
||||||
@@ -813,8 +817,8 @@ export default function RequirementsPage() {
|
|||||||
<svg className="w-12 h-12 text-gray-300 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-12 h-12 text-gray-300 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
<p>No deleted requirements found.</p>
|
<p>{t('deletedPanel.noDeleted')}</p>
|
||||||
<p className="text-sm mt-1">Deleted requirements will appear here.</p>
|
<p className="text-sm mt-1">{t('deletedPanel.deletedWillAppear')}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@@ -834,7 +838,7 @@ export default function RequirementsPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h4 className="font-medium text-gray-800 truncate">
|
<h4 className="font-medium text-gray-800 truncate">
|
||||||
{req.req_name || 'Unnamed Requirement'}
|
{req.req_name || t('deletedPanel.unnamed')}
|
||||||
</h4>
|
</h4>
|
||||||
{req.req_desc && (
|
{req.req_desc && (
|
||||||
<p className="text-sm text-gray-500 mt-1 line-clamp-2">
|
<p className="text-sm text-gray-500 mt-1 line-clamp-2">
|
||||||
@@ -847,17 +851,17 @@ export default function RequirementsPage() {
|
|||||||
<div className="mt-3 pt-3 border-t border-gray-200 text-xs text-gray-500 space-y-1">
|
<div className="mt-3 pt-3 border-t border-gray-200 text-xs text-gray-500 space-y-1">
|
||||||
{req.priority_name && (
|
{req.priority_name && (
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-400">Priority:</span>{' '}
|
<span className="text-gray-400">{t('requirement.priority')}:</span>{' '}
|
||||||
<span className="font-medium">{req.priority_name}</span>
|
<span className="font-medium">{req.priority_name}</span>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-400">Original ID:</span>{' '}
|
<span className="text-gray-400">{t('deletedPanel.originalId')}:</span>{' '}
|
||||||
<span className="font-medium">#{req.original_req_id}</span>
|
<span className="font-medium">#{req.original_req_id}</span>
|
||||||
</p>
|
</p>
|
||||||
{req.deleted_at && (
|
{req.deleted_at && (
|
||||||
<p>
|
<p>
|
||||||
<span className="text-gray-400">Deleted:</span>{' '}
|
<span className="text-gray-400">{t('deletedPanel.deletedAt')}:</span>{' '}
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{new Date(req.deleted_at).toLocaleDateString('en-US', {
|
{new Date(req.deleted_at).toLocaleDateString('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
@@ -884,7 +888,7 @@ export default function RequirementsPage() {
|
|||||||
|
|
||||||
{/* Panel Footer */}
|
{/* Panel Footer */}
|
||||||
<div className="px-6 py-3 border-t border-gray-200 bg-gray-50 text-xs text-gray-500">
|
<div className="px-6 py-3 border-t border-gray-200 bg-gray-50 text-xs text-gray-500">
|
||||||
<p>💡 Deleted requirements are preserved in history for auditing purposes.</p>
|
<p>💡 {t('deletedPanel.footerNote')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -907,7 +911,7 @@ export default function RequirementsPage() {
|
|||||||
<svg className="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
</svg>
|
</svg>
|
||||||
<h2 className="text-lg font-semibold text-gray-800">Delete Requirement</h2>
|
<h2 className="text-lg font-semibold text-gray-800">{t('deleteModal.title')}</h2>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={closeDeleteModal}
|
onClick={closeDeleteModal}
|
||||||
@@ -923,11 +927,11 @@ export default function RequirementsPage() {
|
|||||||
{/* Modal Body */}
|
{/* Modal Body */}
|
||||||
<div className="px-6 py-6">
|
<div className="px-6 py-6">
|
||||||
<p className="text-gray-700">
|
<p className="text-gray-700">
|
||||||
Are you sure you want to delete the requirement{' '}
|
{t('deleteModal.confirmMessage')}{' '}
|
||||||
<span className="font-semibold text-gray-900">"{deleteTarget.name}"</span>?
|
<span className="font-semibold text-gray-900">"{deleteTarget.name}"</span>?
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-500 mt-2">
|
<p className="text-sm text-gray-500 mt-2">
|
||||||
This action will move the requirement to the deleted items. You can view deleted requirements from the "Deleted" panel.
|
{t('deleteModal.explanation')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -938,14 +942,14 @@ export default function RequirementsPage() {
|
|||||||
className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-100"
|
className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-100"
|
||||||
disabled={deleteLoading}
|
disabled={deleteLoading}
|
||||||
>
|
>
|
||||||
Cancel
|
{tCommon('cancel')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleConfirmDelete}
|
onClick={handleConfirmDelete}
|
||||||
className="px-4 py-2 bg-red-600 text-white rounded text-sm font-medium hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="px-4 py-2 bg-red-600 text-white rounded text-sm font-medium hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
disabled={deleteLoading}
|
disabled={deleteLoading}
|
||||||
>
|
>
|
||||||
{deleteLoading ? 'Deleting...' : 'Delete'}
|
{deleteLoading ? t('deleteModal.deleting') : tCommon('delete')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -962,7 +966,7 @@ export default function RequirementsPage() {
|
|||||||
<svg className="w-5 h-5 text-teal-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5 text-teal-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
<h2 className="text-lg font-semibold text-gray-800">Tag Caption</h2>
|
<h2 className="text-lg font-semibold text-gray-800">{t('caption.title')}</h2>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowCaptionModal(false)}
|
onClick={() => setShowCaptionModal(false)}
|
||||||
@@ -992,7 +996,7 @@ export default function RequirementsPage() {
|
|||||||
onClick={() => setShowCaptionModal(false)}
|
onClick={() => setShowCaptionModal(false)}
|
||||||
className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700"
|
className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700"
|
||||||
>
|
>
|
||||||
Close
|
{tCommon('close')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1005,7 +1009,7 @@ export default function RequirementsPage() {
|
|||||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-lg mx-4">
|
<div className="bg-white rounded-lg shadow-xl w-full max-w-lg mx-4">
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
||||||
<h2 className="text-xl font-semibold text-gray-800">New Requirement</h2>
|
<h2 className="text-xl font-semibold text-gray-800">{t('createModal.title')}</h2>
|
||||||
<button
|
<button
|
||||||
onClick={closeCreateModal}
|
onClick={closeCreateModal}
|
||||||
className="text-gray-400 hover:text-gray-600"
|
className="text-gray-400 hover:text-gray-600"
|
||||||
@@ -1029,7 +1033,7 @@ export default function RequirementsPage() {
|
|||||||
{/* Tag Selection */}
|
{/* Tag Selection */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Tag <span className="text-red-500">*</span>
|
{t('createModal.tag')} <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={newReqTagId}
|
value={newReqTagId}
|
||||||
@@ -1037,7 +1041,7 @@ export default function RequirementsPage() {
|
|||||||
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<option value="">Select a tag...</option>
|
<option value="">{t('createModal.selectTag')}</option>
|
||||||
{tags.map((tag) => (
|
{tags.map((tag) => (
|
||||||
<option key={tag.id} value={tag.id}>
|
<option key={tag.id} value={tag.id}>
|
||||||
{tag.tag_code} - {tag.tag_description}
|
{tag.tag_code} - {tag.tag_description}
|
||||||
@@ -1049,13 +1053,13 @@ export default function RequirementsPage() {
|
|||||||
{/* Requirement Name */}
|
{/* Requirement Name */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Name <span className="text-red-500">*</span>
|
{t('createModal.name')} <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={newReqName}
|
value={newReqName}
|
||||||
onChange={(e) => setNewReqName(e.target.value)}
|
onChange={(e) => setNewReqName(e.target.value)}
|
||||||
placeholder="Enter requirement name"
|
placeholder={t('createModal.namePlaceholder')}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -1064,12 +1068,12 @@ export default function RequirementsPage() {
|
|||||||
{/* Description */}
|
{/* Description */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Description
|
{t('createModal.description')}
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={newReqDesc}
|
value={newReqDesc}
|
||||||
onChange={(e) => setNewReqDesc(e.target.value)}
|
onChange={(e) => setNewReqDesc(e.target.value)}
|
||||||
placeholder="Enter requirement description (optional)"
|
placeholder={t('createModal.descriptionPlaceholder')}
|
||||||
rows={3}
|
rows={3}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent resize-none"
|
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent resize-none"
|
||||||
/>
|
/>
|
||||||
@@ -1078,14 +1082,14 @@ export default function RequirementsPage() {
|
|||||||
{/* Priority Selection */}
|
{/* Priority Selection */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Priority
|
{t('createModal.priority')}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={newReqPriorityId}
|
value={newReqPriorityId}
|
||||||
onChange={(e) => setNewReqPriorityId(e.target.value ? Number(e.target.value) : '')}
|
onChange={(e) => setNewReqPriorityId(e.target.value ? Number(e.target.value) : '')}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
className="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
|
||||||
>
|
>
|
||||||
<option value="">Select a priority (optional)...</option>
|
<option value="">{t('createModal.selectPriority')}</option>
|
||||||
{priorities.map((priority) => (
|
{priorities.map((priority) => (
|
||||||
<option key={priority.id} value={priority.id}>
|
<option key={priority.id} value={priority.id}>
|
||||||
{priority.priority_name} ({priority.priority_num})
|
{priority.priority_name} ({priority.priority_num})
|
||||||
@@ -1097,7 +1101,7 @@ export default function RequirementsPage() {
|
|||||||
{/* Groups Selection */}
|
{/* Groups Selection */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Groups
|
{t('createModal.groups')}
|
||||||
</label>
|
</label>
|
||||||
<div className="grid grid-cols-2 gap-2 max-h-40 overflow-y-auto border border-gray-200 rounded p-3">
|
<div className="grid grid-cols-2 gap-2 max-h-40 overflow-y-auto border border-gray-200 rounded p-3">
|
||||||
{groups.map((group) => (
|
{groups.map((group) => (
|
||||||
@@ -1123,14 +1127,14 @@ export default function RequirementsPage() {
|
|||||||
className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-100"
|
className="px-4 py-2 border border-gray-400 rounded text-sm font-medium text-gray-700 hover:bg-gray-100"
|
||||||
disabled={createLoading}
|
disabled={createLoading}
|
||||||
>
|
>
|
||||||
Cancel
|
{tCommon('cancel')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
disabled={createLoading}
|
disabled={createLoading}
|
||||||
>
|
>
|
||||||
{createLoading ? 'Creating...' : 'Create Requirement'}
|
{createLoading ? t('createModal.creating') : t('createModal.createButton')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user