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"
|
||||
},
|
||||
"dependencies": {
|
||||
"i18next": "^25.7.1",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-i18next": "^16.3.5",
|
||||
"react-router-dom": "^6.28.0"
|
||||
},
|
||||
"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 { useTranslation } from 'react-i18next'
|
||||
import Navbar from './Navbar'
|
||||
|
||||
interface LayoutProps {
|
||||
@@ -6,6 +7,8 @@ interface LayoutProps {
|
||||
}
|
||||
|
||||
export default function Layout({ children }: LayoutProps) {
|
||||
const { t } = useTranslation('navbar')
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col">
|
||||
<Navbar />
|
||||
@@ -17,8 +20,7 @@ export default function Layout({ children }: LayoutProps) {
|
||||
<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">
|
||||
<p className="text-center text-sm text-gray-500">
|
||||
© {new Date().getFullYear()} Requirements Periodic Table. All rights
|
||||
reserved.
|
||||
© {new Date().getFullYear()} {t('brand')}.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuth } from '@/hooks'
|
||||
import LanguageSelector from './LanguageSelector'
|
||||
|
||||
export default function Navbar() {
|
||||
const { user, isAuthenticated, login, logout } = useAuth()
|
||||
const { t } = useTranslation('navbar')
|
||||
|
||||
return (
|
||||
<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" />
|
||||
<path d="M9 12l2 2 4-4" />
|
||||
</svg>
|
||||
<span>Requirements Periodic Table</span>
|
||||
<span>{t('brand')}</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Navigation Links */}
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Language Selector */}
|
||||
<LanguageSelector compact />
|
||||
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<Link
|
||||
to="/dashboard"
|
||||
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>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-gray-600">
|
||||
Hello,{' '}
|
||||
{t('hello')},{' '}
|
||||
<span className="font-medium text-gray-900">
|
||||
{user?.full_name || user?.preferred_username}
|
||||
</span>
|
||||
@@ -52,7 +58,7 @@ export default function Navbar() {
|
||||
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"
|
||||
>
|
||||
Logout
|
||||
{t('logout')}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
@@ -61,7 +67,7 @@ export default function Navbar() {
|
||||
onClick={login}
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { default as Layout } from './Layout'
|
||||
export { default as Navbar } from './Navbar'
|
||||
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 './index.css'
|
||||
|
||||
// Initialize i18n before rendering
|
||||
import './i18n'
|
||||
|
||||
// Global fetch interceptor to handle 401 Unauthorized responses
|
||||
// This intercepts all fetch calls and attempts silent refresh before redirecting
|
||||
const originalFetch = window.fetch
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuth, useProject } from '@/hooks'
|
||||
import {
|
||||
projectService,
|
||||
@@ -13,6 +14,8 @@ import {
|
||||
type TabType = 'project' | 'members' | 'relationships'
|
||||
|
||||
export default function AdminPage() {
|
||||
const { t } = useTranslation('admin')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const { user } = useAuth()
|
||||
const { currentProject, setCurrentProject } = useProject()
|
||||
const navigate = useNavigate()
|
||||
@@ -291,13 +294,13 @@ export default function AdminPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-white flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl font-semibold text-gray-700 mb-2">No Project Selected</h2>
|
||||
<p className="text-gray-500 mb-4">Please select a project from the dashboard first.</p>
|
||||
<h2 className="text-xl font-semibold text-gray-700 mb-2">{t('noProject.title')}</h2>
|
||||
<p className="text-gray-500 mb-4">{t('noProject.message')}</p>
|
||||
<button
|
||||
onClick={() => navigate('/dashboard')}
|
||||
className="px-4 py-2 bg-teal-600 text-white rounded hover:bg-teal-700"
|
||||
>
|
||||
Go to Dashboard
|
||||
{t('noProject.goToDashboard')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -308,9 +311,9 @@ export default function AdminPage() {
|
||||
<div className="min-h-screen bg-white">
|
||||
{/* Header */}
|
||||
<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">
|
||||
Managing: <span className="font-medium">{currentProject.project_name}</span>
|
||||
{t('managing')} <span className="font-medium">{currentProject.project_name}</span>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
@@ -326,7 +329,7 @@ export default function AdminPage() {
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
Project Settings
|
||||
{t('tabs.projectSettings')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('members')}
|
||||
@@ -336,7 +339,7 @@ export default function AdminPage() {
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
Member Roles
|
||||
{t('tabs.memberRoles')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('relationships')}
|
||||
@@ -346,7 +349,7 @@ export default function AdminPage() {
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
Relationship Types
|
||||
{t('tabs.relationshipTypes')}
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -361,7 +364,7 @@ export default function AdminPage() {
|
||||
<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" />
|
||||
</svg>
|
||||
Back to Dashboard
|
||||
{t('backToDashboard')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -370,7 +373,7 @@ export default function AdminPage() {
|
||||
{/* Project Settings Tab */}
|
||||
{activeTab === 'project' && (
|
||||
<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">
|
||||
{projectError && (
|
||||
@@ -387,7 +390,7 @@ export default function AdminPage() {
|
||||
|
||||
<div>
|
||||
<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>
|
||||
<input
|
||||
type="text"
|
||||
@@ -400,7 +403,7 @@ export default function AdminPage() {
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Description
|
||||
{t('projectSettings.description')}
|
||||
</label>
|
||||
<textarea
|
||||
value={projectDesc}
|
||||
@@ -416,7 +419,7 @@ export default function AdminPage() {
|
||||
disabled={projectLoading}
|
||||
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>
|
||||
</div>
|
||||
</form>
|
||||
@@ -426,7 +429,7 @@ export default function AdminPage() {
|
||||
{/* Members Tab */}
|
||||
{activeTab === 'members' && (
|
||||
<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 && (
|
||||
<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 ? (
|
||||
<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 ? (
|
||||
<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">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<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">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.user')}</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">{t('memberRoles.tableHeaders.actions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -459,7 +462,7 @@ export default function AdminPage() {
|
||||
<span className="text-sm text-gray-900">{member.sub}</span>
|
||||
{isCurrentUser && (
|
||||
<span className="text-xs bg-teal-100 text-teal-700 px-2 py-0.5 rounded">
|
||||
You
|
||||
{t('memberRoles.youBadge')}
|
||||
</span>
|
||||
)}
|
||||
</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 ${
|
||||
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) => (
|
||||
<option key={role.id} value={role.id}>
|
||||
@@ -494,7 +497,7 @@ export default function AdminPage() {
|
||||
))}
|
||||
</select>
|
||||
{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>
|
||||
</tr>
|
||||
@@ -511,7 +514,7 @@ export default function AdminPage() {
|
||||
{activeTab === 'relationships' && (
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-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
|
||||
onClick={() => {
|
||||
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"
|
||||
>
|
||||
+ Add Type
|
||||
{t('relationshipTypes.addButton')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -530,10 +533,10 @@ export default function AdminPage() {
|
||||
)}
|
||||
|
||||
{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 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
No relationship types defined yet. Create one to link requirements.
|
||||
{t('relationshipTypes.noTypes')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
@@ -546,7 +549,7 @@ export default function AdminPage() {
|
||||
<div className="font-medium text-gray-900">{relType.type_name}</div>
|
||||
{relType.inverse_type_name && (
|
||||
<div className="text-sm text-gray-500">
|
||||
Inverse: {relType.inverse_type_name}
|
||||
{t('relationshipTypes.inverse')} {relType.inverse_type_name}
|
||||
</div>
|
||||
)}
|
||||
{relType.type_description && (
|
||||
@@ -558,13 +561,13 @@ export default function AdminPage() {
|
||||
onClick={() => openEditModal(relType)}
|
||||
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
|
||||
onClick={() => openDeleteModal(relType)}
|
||||
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>
|
||||
</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="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">
|
||||
<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
|
||||
onClick={() => {
|
||||
setShowCreateRelTypeModal(false)
|
||||
@@ -604,13 +607,13 @@ export default function AdminPage() {
|
||||
|
||||
<div>
|
||||
<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>
|
||||
<input
|
||||
type="text"
|
||||
value={relTypeName}
|
||||
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"
|
||||
required
|
||||
/>
|
||||
@@ -618,23 +621,23 @@ export default function AdminPage() {
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Inverse Name
|
||||
{t('createRelTypeModal.inverseName')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={relTypeInverse}
|
||||
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"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Optional. The name shown when viewing from the target requirement.
|
||||
{t('createRelTypeModal.inverseNameHint')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Description
|
||||
{t('createRelTypeModal.description')}
|
||||
</label>
|
||||
<textarea
|
||||
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"
|
||||
disabled={relTypeFormLoading}
|
||||
>
|
||||
Cancel
|
||||
{tCommon('cancel')}
|
||||
</button>
|
||||
<button
|
||||
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={relTypeFormLoading}
|
||||
>
|
||||
{relTypeFormLoading ? 'Creating...' : 'Create'}
|
||||
{relTypeFormLoading ? tCommon('creating') : t('createRelTypeModal.createButton')}
|
||||
</button>
|
||||
</div>
|
||||
</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="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">
|
||||
<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
|
||||
onClick={() => {
|
||||
setShowEditRelTypeModal(false)
|
||||
@@ -700,7 +703,7 @@ export default function AdminPage() {
|
||||
|
||||
<div>
|
||||
<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>
|
||||
<input
|
||||
type="text"
|
||||
@@ -713,7 +716,7 @@ export default function AdminPage() {
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Inverse Name
|
||||
{t('createRelTypeModal.inverseName')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -725,7 +728,7 @@ export default function AdminPage() {
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Description
|
||||
{t('createRelTypeModal.description')}
|
||||
</label>
|
||||
<textarea
|
||||
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"
|
||||
disabled={relTypeFormLoading}
|
||||
>
|
||||
Cancel
|
||||
{tCommon('cancel')}
|
||||
</button>
|
||||
<button
|
||||
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={relTypeFormLoading}
|
||||
>
|
||||
{relTypeFormLoading ? 'Saving...' : 'Save Changes'}
|
||||
{relTypeFormLoading ? tCommon('saving') : t('editRelTypeModal.saveButton')}
|
||||
</button>
|
||||
</div>
|
||||
</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="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
|
||||
<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 className="px-6 py-4">
|
||||
@@ -778,11 +781,11 @@ export default function AdminPage() {
|
||||
)}
|
||||
|
||||
<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>?
|
||||
</p>
|
||||
<p className="text-sm text-red-600 mt-2">
|
||||
⚠️ This will also delete all requirement links using this type.
|
||||
{t('deleteRelTypeModal.warningMessage')}
|
||||
</p>
|
||||
</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"
|
||||
disabled={relTypeFormLoading}
|
||||
>
|
||||
Cancel
|
||||
{tCommon('cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDeleteRelType}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded text-sm font-medium hover:bg-red-700 disabled:opacity-50"
|
||||
disabled={relTypeFormLoading}
|
||||
>
|
||||
{relTypeFormLoading ? 'Deleting...' : 'Delete'}
|
||||
{relTypeFormLoading ? tCommon('deleting') : t('deleteRelTypeModal.deleteButton')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuth, useProject } from '@/hooks'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { groupService, requirementService, Group } from '@/services'
|
||||
import { LanguageSelector } from '@/components'
|
||||
import type { Requirement } from '@/services/requirementService'
|
||||
|
||||
/**
|
||||
@@ -73,6 +75,8 @@ export default function DashboardPage() {
|
||||
const { user, logout } = useAuth()
|
||||
const { projects, currentProject, setCurrentProject, isLoading: projectsLoading, createProject } = useProject()
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation('dashboard')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const [groups, setGroups] = useState<Group[]>([])
|
||||
const [requirements, setRequirements] = useState<Requirement[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -235,9 +239,9 @@ export default function DashboardPage() {
|
||||
{/* Sidebar Header */}
|
||||
<div className="p-6 border-b border-slate-700">
|
||||
<h1 className="text-lg font-semibold text-teal-400">
|
||||
Digital Twin
|
||||
{t('sidebar.title')}
|
||||
</h1>
|
||||
<p className="text-sm text-slate-400">Requirements Tool</p>
|
||||
<p className="text-sm text-slate-400">{t('sidebar.subtitle')}</p>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
@@ -250,11 +254,11 @@ export default function DashboardPage() {
|
||||
<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" />
|
||||
</svg>
|
||||
Create Requirement
|
||||
{t('sidebar.createRequirement')}
|
||||
</button>
|
||||
|
||||
<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>
|
||||
|
||||
{/* 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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
Search Requirements
|
||||
{t('sidebar.searchRequirements')}
|
||||
</button>
|
||||
|
||||
{/* 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">
|
||||
<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>
|
||||
My Requirements
|
||||
{t('sidebar.myRequirements')}
|
||||
</button>
|
||||
|
||||
{/* Reports */}
|
||||
@@ -286,22 +290,22 @@ export default function DashboardPage() {
|
||||
<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" />
|
||||
</svg>
|
||||
Generate Report
|
||||
{t('sidebar.generateReport')}
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
{/* Project Stats Summary at bottom */}
|
||||
{currentProject && !loading && (
|
||||
<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="bg-slate-800 rounded p-2">
|
||||
<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 className="bg-slate-800 rounded p-2">
|
||||
<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>
|
||||
@@ -315,7 +319,7 @@ export default function DashboardPage() {
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Breadcrumb with Project Dropdown */}
|
||||
<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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</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"
|
||||
>
|
||||
{projectsLoading ? (
|
||||
<span className="text-gray-500">Loading...</span>
|
||||
<span className="text-gray-500">{tCommon('loading')}</span>
|
||||
) : currentProject ? (
|
||||
<>
|
||||
{currentProject.project_name}
|
||||
@@ -336,7 +340,7 @@ export default function DashboardPage() {
|
||||
</svg>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-gray-500 italic">No project selected</span>
|
||||
<span className="text-gray-500 italic">{t('projectDropdown.noProjectSelected')}</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
@@ -346,7 +350,7 @@ export default function DashboardPage() {
|
||||
<div className="py-1">
|
||||
{projects.length === 0 ? (
|
||||
<div className="px-4 py-2 text-sm text-gray-500">
|
||||
No projects available
|
||||
{t('projectDropdown.noProjectsAvailable')}
|
||||
</div>
|
||||
) : (
|
||||
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"
|
||||
>
|
||||
+ Create New Project
|
||||
{t('projectDropdown.createNewProject')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -386,14 +390,8 @@ export default function DashboardPage() {
|
||||
|
||||
{/* Right side utilities - grouped tighter */}
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Language Toggle */}
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<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>
|
||||
{/* Language Selector */}
|
||||
<LanguageSelector compact />
|
||||
|
||||
{/* Divider */}
|
||||
<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="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
Admin
|
||||
{t('header.admin')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -422,15 +420,15 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<p className="font-medium text-gray-700">
|
||||
{user?.full_name || user?.preferred_username || 'User'}
|
||||
{user?.full_name || user?.preferred_username || tCommon('user')}
|
||||
</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>
|
||||
<button
|
||||
onClick={logout}
|
||||
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">
|
||||
<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" />
|
||||
</svg>
|
||||
<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">
|
||||
Please select a project from the dropdown above or{' '}
|
||||
{t('noProjectWarning.messagePart1')}{' '}
|
||||
<button
|
||||
onClick={() => setShowCreateProjectModal(true)}
|
||||
className="underline font-medium hover:text-amber-900"
|
||||
>
|
||||
create a new project
|
||||
{t('noProjectWarning.createProject')}
|
||||
</button>
|
||||
{' '}to get started.
|
||||
{' '}{t('noProjectWarning.messagePart2')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -471,10 +469,10 @@ export default function DashboardPage() {
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-xl font-semibold text-gray-800">
|
||||
Quick Search Filters
|
||||
{t('quickFilters.title')}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
Click a category to filter requirements
|
||||
{t('quickFilters.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -483,7 +481,7 @@ export default function DashboardPage() {
|
||||
<div className="flex justify-center items-center min-h-[200px]">
|
||||
<div className="text-center">
|
||||
<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>
|
||||
)}
|
||||
@@ -556,7 +554,7 @@ export default function DashboardPage() {
|
||||
<span
|
||||
className={`ml-auto text-xs ${isLightText ? 'text-white/70' : 'text-black/50'}`}
|
||||
>
|
||||
{stats.total} total
|
||||
{stats.total} {t('quickFilters.total')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -569,7 +567,7 @@ export default function DashboardPage() {
|
||||
{/* Empty State */}
|
||||
{!loading && !error && groups.length === 0 && (
|
||||
<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>
|
||||
@@ -586,10 +584,10 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-800">
|
||||
Needs Attention
|
||||
{t('needsAttention.title')}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
Requirements with denied or partial validation
|
||||
{t('needsAttention.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -597,7 +595,7 @@ export default function DashboardPage() {
|
||||
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"
|
||||
>
|
||||
View All ({requirementsNeedingAttention.length})
|
||||
{t('needsAttention.viewAll')} ({requirementsNeedingAttention.length})
|
||||
<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" />
|
||||
</svg>
|
||||
@@ -653,7 +651,7 @@ export default function DashboardPage() {
|
||||
onClick={handleNeedsAttentionClick}
|
||||
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>
|
||||
</div>
|
||||
)}
|
||||
@@ -672,10 +670,10 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-800">
|
||||
Needs Revalidation
|
||||
{t('needsRevalidation.title')}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
Requirements updated since last validation
|
||||
{t('needsRevalidation.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -683,7 +681,7 @@ export default function DashboardPage() {
|
||||
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"
|
||||
>
|
||||
View All ({requirementsNeedingRevalidation.length})
|
||||
{t('needsRevalidation.viewAll')} ({requirementsNeedingRevalidation.length})
|
||||
<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" />
|
||||
</svg>
|
||||
@@ -706,7 +704,7 @@ export default function DashboardPage() {
|
||||
{req.tag.tag_code}
|
||||
</span>
|
||||
<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>
|
||||
</div>
|
||||
<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}
|
||||
</span>
|
||||
{req.validated_by && (
|
||||
<span className="truncate max-w-[120px]" title={`Last validated by ${req.validated_by}`}>
|
||||
by {req.validated_by}
|
||||
<span className="truncate max-w-[120px]" title={`${t('needsRevalidation.lastValidatedBy')} ${req.validated_by}`}>
|
||||
{t('needsRevalidation.by')} {req.validated_by}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -734,7 +732,7 @@ export default function DashboardPage() {
|
||||
onClick={handleNeedsRevalidationClick}
|
||||
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>
|
||||
</div>
|
||||
)}
|
||||
@@ -751,9 +749,9 @@ export default function DashboardPage() {
|
||||
</svg>
|
||||
</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">
|
||||
No requirements need attention. All validations are either approved or pending review.
|
||||
{t('allClear.message')}
|
||||
</p>
|
||||
</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">
|
||||
{/* Modal Header */}
|
||||
<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
|
||||
onClick={() => {
|
||||
setShowCreateProjectModal(false)
|
||||
@@ -805,13 +803,13 @@ export default function DashboardPage() {
|
||||
{/* Project Name */}
|
||||
<div>
|
||||
<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>
|
||||
<input
|
||||
type="text"
|
||||
value={newProjectName}
|
||||
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"
|
||||
required
|
||||
/>
|
||||
@@ -820,12 +818,12 @@ export default function DashboardPage() {
|
||||
{/* Project Description */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Description
|
||||
{t('createProject.description')}
|
||||
</label>
|
||||
<textarea
|
||||
value={newProjectDesc}
|
||||
onChange={(e) => setNewProjectDesc(e.target.value)}
|
||||
placeholder="Enter project description (optional)"
|
||||
placeholder={t('createProject.descriptionPlaceholder')}
|
||||
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"
|
||||
/>
|
||||
@@ -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"
|
||||
disabled={createProjectLoading}
|
||||
>
|
||||
Cancel
|
||||
{tCommon('cancel')}
|
||||
</button>
|
||||
<button
|
||||
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"
|
||||
disabled={createProjectLoading}
|
||||
>
|
||||
{createProjectLoading ? 'Creating...' : 'Create Project'}
|
||||
{createProjectLoading ? t('createProject.creating') : t('createProject.createButton')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -862,4 +860,3 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuth } from '@/hooks'
|
||||
|
||||
export default function HomePage() {
|
||||
const { isAuthenticated, login, isLoading } = useAuth()
|
||||
const { t: tHome } = useTranslation('home')
|
||||
const { t: tNavbar } = useTranslation('navbar')
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -32,10 +35,10 @@ export default function HomePage() {
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl">
|
||||
Requirements Periodic Table
|
||||
{tHome('title')}
|
||||
</h1>
|
||||
<p className="mx-auto mt-4 max-w-md text-lg text-gray-600">
|
||||
Manage and track your project requirements.
|
||||
{tHome('subtitle')}
|
||||
</p>
|
||||
|
||||
<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"
|
||||
/>
|
||||
</svg>
|
||||
Login
|
||||
{tNavbar('login')}
|
||||
</button>
|
||||
) : (
|
||||
<a
|
||||
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"
|
||||
>
|
||||
Go to Dashboard
|
||||
{tHome('goToDashboard')}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuth, useProject } from '@/hooks'
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import { requirementService, validationService, relationshipService, commentService, tagService, priorityService, groupService, requirementStatusService } from '@/services'
|
||||
@@ -40,6 +41,8 @@ interface TimelineEvent {
|
||||
}
|
||||
|
||||
export default function RequirementDetailPage() {
|
||||
const { t } = useTranslation('requirementDetail')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const { user, logout, isAuditor } = useAuth()
|
||||
const { currentProject } = useProject()
|
||||
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="text-center">
|
||||
<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>
|
||||
)
|
||||
@@ -730,10 +733,10 @@ export default function RequirementDetailPage() {
|
||||
<div className="min-h-screen bg-white flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl font-semibold text-gray-700 mb-4">
|
||||
{error || 'Requirement not found'}
|
||||
{error || t('requirementNotFound')}
|
||||
</h2>
|
||||
<Link to="/requirements" className="text-teal-600 hover:underline">
|
||||
Back to Requirements
|
||||
{t('backToRequirements')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -741,12 +744,12 @@ export default function RequirementDetailPage() {
|
||||
}
|
||||
|
||||
const tabs: { id: TabType; label: string }[] = [
|
||||
{ id: 'description', label: 'Description' },
|
||||
{ id: 'relationships', label: 'Relationships' },
|
||||
{ id: 'acceptance-criteria', label: 'Acceptance Criteria' },
|
||||
{ id: 'shared-comments', label: 'Shared Comments' },
|
||||
{ id: 'validate', label: 'Validate' },
|
||||
{ id: 'history', label: 'History' },
|
||||
{ id: 'description', label: t('tabs.description') },
|
||||
{ id: 'relationships', label: t('tabs.relationships') },
|
||||
{ id: 'acceptance-criteria', label: t('tabs.acceptanceCriteria') },
|
||||
{ id: 'shared-comments', label: t('tabs.sharedComments') },
|
||||
{ id: 'validate', label: t('tabs.validate') },
|
||||
{ id: 'history', label: t('tabs.history') },
|
||||
]
|
||||
|
||||
// Get display values from the requirement data
|
||||
@@ -759,7 +762,7 @@ export default function RequirementDetailPage() {
|
||||
case 'description':
|
||||
return (
|
||||
<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 */}
|
||||
{isDraftStatus && (
|
||||
@@ -767,18 +770,18 @@ export default function RequirementDetailPage() {
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-amber-600 text-lg">📝</span>
|
||||
<div>
|
||||
<p className="font-semibold text-amber-800">Draft Requirement</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="font-semibold text-amber-800">{t('draft.title')}</p>
|
||||
<p className="text-sm text-amber-700">{t('draft.message')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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 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 ${
|
||||
isDraftStatus
|
||||
? 'bg-amber-100 text-amber-800 border border-amber-300'
|
||||
@@ -788,7 +791,7 @@ export default function RequirementDetailPage() {
|
||||
</span>
|
||||
</p>
|
||||
<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)}`}>
|
||||
{validationStatus}
|
||||
</span>
|
||||
@@ -802,7 +805,7 @@ export default function RequirementDetailPage() {
|
||||
)}
|
||||
</p>
|
||||
<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 ? (
|
||||
<span className="text-gray-700">{requirement.author_username}</span>
|
||||
) : (
|
||||
@@ -811,22 +814,22 @@ export default function RequirementDetailPage() {
|
||||
</p>
|
||||
{requirement.last_editor_username && (
|
||||
<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>
|
||||
</p>
|
||||
)}
|
||||
{requirement.created_at && (
|
||||
<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>
|
||||
)}
|
||||
{requirement.updated_at && (
|
||||
<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>
|
||||
)}
|
||||
<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>
|
||||
{/* Hide Edit button for auditors */}
|
||||
{!isAuditor && (
|
||||
@@ -835,7 +838,7 @@ export default function RequirementDetailPage() {
|
||||
onClick={openEditModal}
|
||||
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>
|
||||
</div>
|
||||
)}
|
||||
@@ -846,43 +849,43 @@ export default function RequirementDetailPage() {
|
||||
return (
|
||||
<div>
|
||||
<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 && (
|
||||
<button
|
||||
onClick={openAddRelationshipModal}
|
||||
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"
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{relationshipTypes.length === 0 && !relationshipsLoading && (
|
||||
<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>
|
||||
)}
|
||||
|
||||
{relationshipsLoading ? (
|
||||
<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>
|
||||
<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>
|
||||
) : 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">
|
||||
<table className="w-full border border-gray-200 rounded">
|
||||
<thead className="bg-gray-50">
|
||||
<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">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">Created By</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">Actions</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">{t('relationships.tableHeaders.type')}</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">{t('relationships.tableHeaders.createdBy')}</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">{t('relationships.tableHeaders.actions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
@@ -894,7 +897,7 @@ export default function RequirementDetailPage() {
|
||||
? 'bg-blue-100 text-blue-800'
|
||||
: 'bg-purple-100 text-purple-800'
|
||||
}`}>
|
||||
{link.direction === 'outgoing' ? '→ Outgoing' : '← Incoming'}
|
||||
{link.direction === 'outgoing' ? t('relationships.outgoing') : t('relationships.incoming')}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-700">
|
||||
@@ -944,12 +947,12 @@ export default function RequirementDetailPage() {
|
||||
case 'acceptance-criteria':
|
||||
return (
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-gray-800 mb-4">Acceptance Criteria</h3>
|
||||
<p className="text-gray-500">No acceptance criteria defined yet.</p>
|
||||
<h3 className="text-xl font-bold text-gray-800 mb-4">{t('acceptanceCriteria.title')}</h3>
|
||||
<p className="text-gray-500">{t('acceptanceCriteria.noCriteria')}</p>
|
||||
{!isAuditor && (
|
||||
<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">
|
||||
Add Criterion
|
||||
{t('acceptanceCriteria.addButton')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -959,14 +962,14 @@ export default function RequirementDetailPage() {
|
||||
case 'shared-comments':
|
||||
return (
|
||||
<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 */}
|
||||
<div className="mb-6 p-4 border border-gray-200 rounded bg-gray-50">
|
||||
<textarea
|
||||
value={newCommentText}
|
||||
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"
|
||||
rows={3}
|
||||
disabled={postingComment}
|
||||
@@ -977,7 +980,7 @@ export default function RequirementDetailPage() {
|
||||
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"
|
||||
>
|
||||
{postingComment ? 'Posting...' : 'Post Comment'}
|
||||
{postingComment ? t('comments.posting') : t('comments.postButton')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -986,10 +989,10 @@ export default function RequirementDetailPage() {
|
||||
{commentsLoading ? (
|
||||
<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>
|
||||
<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>
|
||||
) : 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">
|
||||
{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"
|
||||
>
|
||||
{replyingToCommentId === comment.id ? 'Cancel Reply' : 'Reply'}
|
||||
{replyingToCommentId === comment.id ? t('comments.cancelReply') : t('comments.reply')}
|
||||
</button>
|
||||
|
||||
{/* Reply Form */}
|
||||
@@ -1047,7 +1050,7 @@ export default function RequirementDetailPage() {
|
||||
<textarea
|
||||
value={replyText}
|
||||
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"
|
||||
rows={2}
|
||||
disabled={postingReply}
|
||||
@@ -1058,7 +1061,7 @@ export default function RequirementDetailPage() {
|
||||
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"
|
||||
>
|
||||
{postingReply ? 'Posting...' : 'Post Reply'}
|
||||
{postingReply ? t('comments.posting') : t('comments.postReply')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1117,25 +1120,25 @@ export default function RequirementDetailPage() {
|
||||
case 'validate':
|
||||
return (
|
||||
<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 */}
|
||||
<div className="mb-6 p-4 bg-gray-50 border border-gray-200 rounded">
|
||||
<div className="flex items-center justify-between">
|
||||
<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)}`}>
|
||||
{validationStatus}
|
||||
</span>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
{requirement.validated_by && (
|
||||
<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()}`}
|
||||
</p>
|
||||
)}
|
||||
@@ -1144,7 +1147,7 @@ export default function RequirementDetailPage() {
|
||||
<p className="text-sm text-orange-800 flex items-center gap-2">
|
||||
<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>
|
||||
</p>
|
||||
</div>
|
||||
@@ -1154,7 +1157,7 @@ export default function RequirementDetailPage() {
|
||||
{/* Validation Form - Only for auditors */}
|
||||
{isAuditor && (
|
||||
<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 && (
|
||||
<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 */}
|
||||
<div>
|
||||
<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>
|
||||
<select
|
||||
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"
|
||||
disabled={validationLoading}
|
||||
>
|
||||
<option value="">Select a status...</option>
|
||||
<option value="">{t('validate.selectStatus')}</option>
|
||||
{validationStatuses.map((status) => (
|
||||
<option key={status.id} value={status.id}>
|
||||
{status.status_name}
|
||||
@@ -1186,12 +1189,12 @@ export default function RequirementDetailPage() {
|
||||
{/* Comment */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Comment
|
||||
{t('validate.commentLabel')}
|
||||
</label>
|
||||
<textarea
|
||||
value={validationComment}
|
||||
onChange={(e) => setValidationComment(e.target.value)}
|
||||
placeholder="Add a comment explaining your decision (optional but recommended)"
|
||||
placeholder={t('validate.commentPlaceholder')}
|
||||
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"
|
||||
disabled={validationLoading}
|
||||
@@ -1204,7 +1207,7 @@ export default function RequirementDetailPage() {
|
||||
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"
|
||||
>
|
||||
{validationLoading ? 'Submitting...' : 'Submit Validation'}
|
||||
{validationLoading ? t('validate.submitting') : t('validate.submitButton')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1212,25 +1215,25 @@ export default function RequirementDetailPage() {
|
||||
|
||||
{/* Validation History */}
|
||||
<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 ? (
|
||||
<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>
|
||||
<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>
|
||||
) : 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">
|
||||
<table className="w-full border border-gray-200 rounded">
|
||||
<thead className="bg-gray-50">
|
||||
<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">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">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.date')}</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">{t('validate.tableHeaders.version')}</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">{t('validate.tableHeaders.comment')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
@@ -1252,7 +1255,7 @@ export default function RequirementDetailPage() {
|
||||
<span className="flex items-center gap-1">
|
||||
v{validation.req_version_snapshot}
|
||||
{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>
|
||||
)}
|
||||
@@ -1262,7 +1265,7 @@ export default function RequirementDetailPage() {
|
||||
{validation.validator_username}
|
||||
</td>
|
||||
<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>
|
||||
</tr>
|
||||
)
|
||||
@@ -1290,11 +1293,11 @@ export default function RequirementDetailPage() {
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
<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 ? (
|
||||
<span className="text-gray-800">{requirement.author_username}</span>
|
||||
) : (
|
||||
@@ -1310,10 +1313,10 @@ export default function RequirementDetailPage() {
|
||||
{reqHistoryLoading ? (
|
||||
<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>
|
||||
<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>
|
||||
) : 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">
|
||||
{timelineEvents.map((event) => {
|
||||
@@ -1346,7 +1349,7 @@ export default function RequirementDetailPage() {
|
||||
<span className="font-semibold text-gray-800">
|
||||
v{oldItem.version} → v{newItem ? newItem.version : requirement.version}
|
||||
</span>
|
||||
<span className="text-sm text-gray-600">Requirement edited</span>
|
||||
<span className="text-sm text-gray-600">{t('history.requirementEdited')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-gray-500">
|
||||
{oldItem.edited_by_username && (
|
||||
@@ -1365,7 +1368,7 @@ export default function RequirementDetailPage() {
|
||||
{isExpanded && (
|
||||
<div className="px-4 py-3 border-t border-gray-200 bg-white space-y-4">
|
||||
{!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 */}
|
||||
@@ -1463,7 +1466,7 @@ export default function RequirementDetailPage() {
|
||||
}}
|
||||
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>
|
||||
)}
|
||||
</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 gap-3">
|
||||
<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">
|
||||
{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 gap-3">
|
||||
<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">
|
||||
{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 gap-3">
|
||||
<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">
|
||||
{group.groupName ? (
|
||||
<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 gap-3">
|
||||
<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">
|
||||
{group.groupName ? (
|
||||
<span
|
||||
@@ -1650,7 +1653,7 @@ export default function RequirementDetailPage() {
|
||||
{/* Header */}
|
||||
<header className="py-6 text-center">
|
||||
<h1 className="text-3xl font-semibold text-teal-700">
|
||||
Digital Twin Requirements Tool
|
||||
{t('pageTitle')}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
@@ -1659,22 +1662,13 @@ export default function RequirementDetailPage() {
|
||||
<div className="flex items-center justify-between max-w-7xl mx-auto">
|
||||
{/* Breadcrumb */}
|
||||
<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>
|
||||
<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>
|
||||
<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="font-semibold text-gray-900">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>
|
||||
<span className="font-semibold text-gray-900">{t('breadcrumb.details')} {tagCode}</span>
|
||||
</div>
|
||||
|
||||
{/* 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" />
|
||||
</svg>
|
||||
<span className="text-sm text-gray-700">
|
||||
{user?.full_name || user?.preferred_username || 'User'}{' '}
|
||||
<span className="text-gray-500">({user?.role || 'user'})</span>
|
||||
{user?.full_name || user?.preferred_username || tCommon('user')}{' '}
|
||||
<span className="text-gray-500">({user?.role || tCommon('user')})</span>
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={logout}
|
||||
className="px-3 py-1 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
Logout
|
||||
{tCommon('logout')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1706,7 +1700,7 @@ export default function RequirementDetailPage() {
|
||||
{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">
|
||||
<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>
|
||||
)}
|
||||
|
||||
@@ -1788,7 +1782,7 @@ export default function RequirementDetailPage() {
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-lg mx-4">
|
||||
{/* Modal Header */}
|
||||
<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
|
||||
onClick={closeAddRelationshipModal}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
@@ -1808,7 +1802,7 @@ export default function RequirementDetailPage() {
|
||||
{/* Relationship Type Selection */}
|
||||
<div>
|
||||
<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>
|
||||
<select
|
||||
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"
|
||||
disabled={addLinkLoading}
|
||||
>
|
||||
<option value="">Select a relationship type...</option>
|
||||
<option value="">{t('addRelationshipModal.selectType')}</option>
|
||||
{relationshipTypes.map((type) => (
|
||||
<option key={type.id} value={type.id}>
|
||||
{type.type_name}
|
||||
@@ -1829,7 +1823,7 @@ export default function RequirementDetailPage() {
|
||||
{/* Target Requirement Search */}
|
||||
<div>
|
||||
<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>
|
||||
<div className="relative">
|
||||
<input
|
||||
@@ -1839,7 +1833,7 @@ export default function RequirementDetailPage() {
|
||||
setTargetSearchQuery(e.target.value)
|
||||
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"
|
||||
disabled={addLinkLoading}
|
||||
/>
|
||||
@@ -1868,7 +1862,7 @@ export default function RequirementDetailPage() {
|
||||
</div>
|
||||
{selectedTarget && (
|
||||
<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>
|
||||
)}
|
||||
</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"
|
||||
disabled={addLinkLoading}
|
||||
>
|
||||
Cancel
|
||||
{tCommon('cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -1890,7 +1884,7 @@ export default function RequirementDetailPage() {
|
||||
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"
|
||||
>
|
||||
{addLinkLoading ? 'Creating...' : 'Create Relationship'}
|
||||
{addLinkLoading ? tCommon('creating') : t('addRelationshipModal.createButton')}
|
||||
</button>
|
||||
</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">
|
||||
{/* Modal Header */}
|
||||
<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
|
||||
onClick={closeEditModal}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
@@ -1923,14 +1917,14 @@ export default function RequirementDetailPage() {
|
||||
{editOptionsLoading ? (
|
||||
<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>
|
||||
<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>
|
||||
) : (
|
||||
<>
|
||||
{/* Name */}
|
||||
<div>
|
||||
<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>
|
||||
<input
|
||||
type="text"
|
||||
@@ -1944,7 +1938,7 @@ export default function RequirementDetailPage() {
|
||||
{/* Tag */}
|
||||
<div>
|
||||
<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>
|
||||
<select
|
||||
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"
|
||||
disabled={editLoading}
|
||||
>
|
||||
<option value="">Select a tag...</option>
|
||||
<option value="">{t('editModal.selectTag')}</option>
|
||||
{availableTags.map((tag) => (
|
||||
<option key={tag.id} value={tag.id}>
|
||||
{tag.tag_code} - {tag.tag_description}
|
||||
@@ -1964,7 +1958,7 @@ export default function RequirementDetailPage() {
|
||||
{/* Priority */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Priority
|
||||
{t('editModal.priority')}
|
||||
</label>
|
||||
<select
|
||||
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"
|
||||
disabled={editLoading}
|
||||
>
|
||||
<option value="">No priority</option>
|
||||
<option value="">{t('editModal.noPriority')}</option>
|
||||
{availablePriorities.map((priority) => (
|
||||
<option key={priority.id} value={priority.id}>
|
||||
{priority.priority_name}
|
||||
@@ -1984,7 +1978,7 @@ export default function RequirementDetailPage() {
|
||||
{/* Status */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Status
|
||||
{t('editModal.status')}
|
||||
</label>
|
||||
<select
|
||||
value={editStatusId}
|
||||
@@ -1999,14 +1993,14 @@ export default function RequirementDetailPage() {
|
||||
))}
|
||||
</select>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Draft requirements are not finalized and marked with a visual indicator.
|
||||
{t('editModal.draftNote')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Description
|
||||
{t('editModal.description')}
|
||||
</label>
|
||||
<textarea
|
||||
value={editReqDesc}
|
||||
@@ -2020,10 +2014,10 @@ export default function RequirementDetailPage() {
|
||||
{/* Groups */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Groups
|
||||
{t('editModal.groups')}
|
||||
</label>
|
||||
{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">
|
||||
{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"
|
||||
disabled={editLoading}
|
||||
>
|
||||
Cancel
|
||||
{tCommon('cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -2072,7 +2066,7 @@ export default function RequirementDetailPage() {
|
||||
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"
|
||||
>
|
||||
{editLoading ? 'Saving...' : 'Save Changes'}
|
||||
{editLoading ? tCommon('saving') : tCommon('saveChanges')}
|
||||
</button>
|
||||
</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"
|
||||
disabled={deleteConfirmLoading}
|
||||
>
|
||||
Cancel
|
||||
{tCommon('cancel')}
|
||||
</button>
|
||||
<button
|
||||
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"
|
||||
disabled={deleteConfirmLoading}
|
||||
>
|
||||
{deleteConfirmLoading ? 'Deleting...' : 'Delete'}
|
||||
{deleteConfirmLoading ? tCommon('deleting') : tCommon('delete')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuth, useProject } from '@/hooks'
|
||||
import { useSearchParams, Link, useNavigate, useLocation } from 'react-router-dom'
|
||||
import { groupService, tagService, requirementService, priorityService } from '@/services'
|
||||
import { LanguageSelector } from '@/components'
|
||||
import type { Group } from '@/services/groupService'
|
||||
import type { Tag } from '@/services/tagService'
|
||||
import type { Priority } from '@/services/priorityService'
|
||||
@@ -30,6 +32,8 @@ const isDraftStatus = (statusCode: string | undefined): boolean => {
|
||||
}
|
||||
|
||||
export default function RequirementsPage() {
|
||||
const { t } = useTranslation('requirements')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const { user, logout, isAuditor } = useAuth()
|
||||
const { currentProject, isLoading: projectLoading } = useProject()
|
||||
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="text-center">
|
||||
<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>
|
||||
)
|
||||
@@ -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">
|
||||
<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>
|
||||
<h2 className="text-xl font-semibold text-gray-700 mb-2">No Project Selected</h2>
|
||||
<p className="text-gray-500 mb-4">Please select a project from the dashboard to view requirements.</p>
|
||||
<h2 className="text-xl font-semibold text-gray-700 mb-2">{t('noProjectSelected.title')}</h2>
|
||||
<p className="text-gray-500 mb-4">{t('noProjectSelected.message')}</p>
|
||||
<button
|
||||
onClick={() => navigate('/dashboard')}
|
||||
className="px-4 py-2 bg-teal-600 text-white rounded hover:bg-teal-700"
|
||||
>
|
||||
Go to Dashboard
|
||||
{t('noProjectSelected.goToDashboard')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -401,7 +405,7 @@ export default function RequirementsPage() {
|
||||
onClick={() => window.location.reload()}
|
||||
className="px-4 py-2 bg-teal-600 text-white rounded hover:bg-teal-700"
|
||||
>
|
||||
Retry
|
||||
{tCommon('retry')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -413,7 +417,7 @@ export default function RequirementsPage() {
|
||||
{/* Header */}
|
||||
<header className="py-6 text-center">
|
||||
<h1 className="text-3xl font-semibold text-teal-700">
|
||||
Digital Twin Requirements Tool
|
||||
{t('header.title')}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
@@ -422,21 +426,15 @@ export default function RequirementsPage() {
|
||||
<div className="flex items-center justify-between max-w-7xl mx-auto">
|
||||
{/* Breadcrumb */}
|
||||
<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>
|
||||
<Link to="/dashboard" className="text-gray-600 hover:underline">{currentProject.project_name}</Link>
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
{/* Language Selector */}
|
||||
<LanguageSelector compact />
|
||||
|
||||
{/* User Info */}
|
||||
<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" />
|
||||
</svg>
|
||||
<span className="text-sm text-gray-700">
|
||||
{user?.full_name || user?.preferred_username || 'User'}{' '}
|
||||
<span className="text-gray-500">({user?.role || 'user'})</span>
|
||||
{user?.full_name || user?.preferred_username || tCommon('user')}{' '}
|
||||
<span className="text-gray-500">({user?.role || tCommon('user').toLowerCase()})</span>
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={logout}
|
||||
className="px-3 py-1 border border-gray-400 rounded text-sm text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
Logout
|
||||
{tCommon('logout')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -470,7 +468,7 @@ export default function RequirementsPage() {
|
||||
onClick={openCreateModal}
|
||||
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
|
||||
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">
|
||||
<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>
|
||||
Deleted
|
||||
{t('deleted')}
|
||||
{deletedRequirements.length > 0 && (
|
||||
<span className="bg-red-500 text-white text-xs px-1.5 py-0.5 rounded-full">
|
||||
{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">
|
||||
<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>
|
||||
Deleted
|
||||
{t('deleted')}
|
||||
{deletedRequirements.length > 0 && (
|
||||
<span className="bg-red-500 text-white text-xs px-1.5 py-0.5 rounded-full">
|
||||
{deletedRequirements.length}
|
||||
@@ -519,7 +517,7 @@ export default function RequirementsPage() {
|
||||
<div className="flex gap-2 mb-6">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search for a requirement tag or title"
|
||||
placeholder={t('searchPlaceholder')}
|
||||
value={searchQuery}
|
||||
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"
|
||||
@@ -528,19 +526,19 @@ export default function RequirementsPage() {
|
||||
onClick={handleSearch}
|
||||
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
|
||||
onClick={handleClear}
|
||||
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>
|
||||
</div>
|
||||
|
||||
{/* Filter Group */}
|
||||
<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">
|
||||
{groups.map((group) => (
|
||||
<label key={group.id} className="flex items-center gap-2 cursor-pointer">
|
||||
@@ -559,16 +557,16 @@ export default function RequirementsPage() {
|
||||
{/* Filter Validation Status */}
|
||||
<div className="mb-6">
|
||||
<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">
|
||||
{needsRevalidationFilter && (
|
||||
<span className="text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded-full">
|
||||
Showing: Needs Revalidation
|
||||
{t('filters.showingNeedsRevalidation')}
|
||||
</span>
|
||||
)}
|
||||
{selectedValidationStatuses.includes('__NEEDS_ATTENTION__') && (
|
||||
<span className="text-xs bg-amber-100 text-amber-700 px-2 py-1 rounded-full">
|
||||
Showing: Needs Attention
|
||||
{t('filters.showingNeedsAttention')}
|
||||
</span>
|
||||
)}
|
||||
</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">
|
||||
<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>
|
||||
Needs Revalidation
|
||||
{t('filters.needsRevalidation')}
|
||||
</button>
|
||||
)}
|
||||
{['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',
|
||||
'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 (
|
||||
<button
|
||||
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]}`}
|
||||
>
|
||||
{status}
|
||||
{statusTranslations[status]}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
@@ -625,18 +629,18 @@ export default function RequirementsPage() {
|
||||
{/* Order By */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<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
|
||||
value={orderBy}
|
||||
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"
|
||||
>
|
||||
<option value="Date">Date</option>
|
||||
<option value="Priority">Priority</option>
|
||||
<option value="Name">Name</option>
|
||||
<option value="Date">{t('orderBy.date')}</option>
|
||||
<option value="Priority">{t('orderBy.priority')}</option>
|
||||
<option value="Name">{t('orderBy.name')}</option>
|
||||
</select>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -644,12 +648,12 @@ export default function RequirementsPage() {
|
||||
<button
|
||||
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"
|
||||
title="View tag captions"
|
||||
title={t('caption.viewCaptions')}
|
||||
>
|
||||
<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" />
|
||||
</svg>
|
||||
Caption
|
||||
{t('caption.title')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -678,9 +682,9 @@ export default function RequirementsPage() {
|
||||
{isDraft && (
|
||||
<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"
|
||||
title="This requirement is still in draft and not finalized"
|
||||
title={t('requirement.draftTooltip')}
|
||||
>
|
||||
📝 Draft
|
||||
📝 {t('requirement.draft')}
|
||||
</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"
|
||||
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 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>
|
||||
)}
|
||||
</div>
|
||||
@@ -730,22 +734,22 @@ export default function RequirementsPage() {
|
||||
{validationStatus}
|
||||
</span>
|
||||
{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">
|
||||
⚠ Stale
|
||||
<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')}>
|
||||
⚠ {t('requirement.stale')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Priority and Version */}
|
||||
<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-600">Version: {req.version}</p>
|
||||
<p className="text-sm text-gray-700 whitespace-nowrap">{t('requirement.priority')}: {priorityName}</p>
|
||||
<p className="text-sm text-gray-600">{t('requirement.version')}: {req.version}</p>
|
||||
</div>
|
||||
|
||||
{/* Spacer to push buttons to the right */}
|
||||
@@ -757,14 +761,14 @@ export default function RequirementsPage() {
|
||||
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"
|
||||
>
|
||||
Details
|
||||
{tCommon('details')}
|
||||
</button>
|
||||
{!isAuditor && (
|
||||
<button
|
||||
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"
|
||||
>
|
||||
Remove
|
||||
{tCommon('remove')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -774,7 +778,7 @@ export default function RequirementsPage() {
|
||||
|
||||
{sortedRequirements.length === 0 && (
|
||||
<div className="text-center py-12 text-gray-500">
|
||||
No requirements found matching your criteria.
|
||||
{t('noRequirementsFound')}
|
||||
</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">
|
||||
<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>
|
||||
<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>
|
||||
<button
|
||||
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">
|
||||
<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>
|
||||
<p>No deleted requirements found.</p>
|
||||
<p className="text-sm mt-1">Deleted requirements will appear here.</p>
|
||||
<p>{t('deletedPanel.noDeleted')}</p>
|
||||
<p className="text-sm mt-1">{t('deletedPanel.deletedWillAppear')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
@@ -834,7 +838,7 @@ export default function RequirementsPage() {
|
||||
</span>
|
||||
</div>
|
||||
<h4 className="font-medium text-gray-800 truncate">
|
||||
{req.req_name || 'Unnamed Requirement'}
|
||||
{req.req_name || t('deletedPanel.unnamed')}
|
||||
</h4>
|
||||
{req.req_desc && (
|
||||
<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">
|
||||
{req.priority_name && (
|
||||
<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>
|
||||
</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>
|
||||
</p>
|
||||
{req.deleted_at && (
|
||||
<p>
|
||||
<span className="text-gray-400">Deleted:</span>{' '}
|
||||
<span className="text-gray-400">{t('deletedPanel.deletedAt')}:</span>{' '}
|
||||
<span className="font-medium">
|
||||
{new Date(req.deleted_at).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
@@ -884,7 +888,7 @@ export default function RequirementsPage() {
|
||||
|
||||
{/* Panel Footer */}
|
||||
<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>
|
||||
)}
|
||||
@@ -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">
|
||||
<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>
|
||||
<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>
|
||||
<button
|
||||
onClick={closeDeleteModal}
|
||||
@@ -923,11 +927,11 @@ export default function RequirementsPage() {
|
||||
{/* Modal Body */}
|
||||
<div className="px-6 py-6">
|
||||
<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>?
|
||||
</p>
|
||||
<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>
|
||||
</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"
|
||||
disabled={deleteLoading}
|
||||
>
|
||||
Cancel
|
||||
{tCommon('cancel')}
|
||||
</button>
|
||||
<button
|
||||
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"
|
||||
disabled={deleteLoading}
|
||||
>
|
||||
{deleteLoading ? 'Deleting...' : 'Delete'}
|
||||
{deleteLoading ? t('deleteModal.deleting') : tCommon('delete')}
|
||||
</button>
|
||||
</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">
|
||||
<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>
|
||||
<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>
|
||||
<button
|
||||
onClick={() => setShowCaptionModal(false)}
|
||||
@@ -992,7 +996,7 @@ export default function RequirementsPage() {
|
||||
onClick={() => setShowCaptionModal(false)}
|
||||
className="px-4 py-2 bg-teal-600 text-white rounded text-sm font-medium hover:bg-teal-700"
|
||||
>
|
||||
Close
|
||||
{tCommon('close')}
|
||||
</button>
|
||||
</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">
|
||||
{/* Modal Header */}
|
||||
<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
|
||||
onClick={closeCreateModal}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
@@ -1029,7 +1033,7 @@ export default function RequirementsPage() {
|
||||
{/* Tag Selection */}
|
||||
<div>
|
||||
<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>
|
||||
<select
|
||||
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"
|
||||
required
|
||||
>
|
||||
<option value="">Select a tag...</option>
|
||||
<option value="">{t('createModal.selectTag')}</option>
|
||||
{tags.map((tag) => (
|
||||
<option key={tag.id} value={tag.id}>
|
||||
{tag.tag_code} - {tag.tag_description}
|
||||
@@ -1049,13 +1053,13 @@ export default function RequirementsPage() {
|
||||
{/* Requirement Name */}
|
||||
<div>
|
||||
<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>
|
||||
<input
|
||||
type="text"
|
||||
value={newReqName}
|
||||
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"
|
||||
required
|
||||
/>
|
||||
@@ -1064,12 +1068,12 @@ export default function RequirementsPage() {
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Description
|
||||
{t('createModal.description')}
|
||||
</label>
|
||||
<textarea
|
||||
value={newReqDesc}
|
||||
onChange={(e) => setNewReqDesc(e.target.value)}
|
||||
placeholder="Enter requirement description (optional)"
|
||||
placeholder={t('createModal.descriptionPlaceholder')}
|
||||
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"
|
||||
/>
|
||||
@@ -1078,14 +1082,14 @@ export default function RequirementsPage() {
|
||||
{/* Priority Selection */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Priority
|
||||
{t('createModal.priority')}
|
||||
</label>
|
||||
<select
|
||||
value={newReqPriorityId}
|
||||
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"
|
||||
>
|
||||
<option value="">Select a priority (optional)...</option>
|
||||
<option value="">{t('createModal.selectPriority')}</option>
|
||||
{priorities.map((priority) => (
|
||||
<option key={priority.id} value={priority.id}>
|
||||
{priority.priority_name} ({priority.priority_num})
|
||||
@@ -1097,7 +1101,7 @@ export default function RequirementsPage() {
|
||||
{/* Groups Selection */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Groups
|
||||
{t('createModal.groups')}
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-2 max-h-40 overflow-y-auto border border-gray-200 rounded p-3">
|
||||
{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"
|
||||
disabled={createLoading}
|
||||
>
|
||||
Cancel
|
||||
{tCommon('cancel')}
|
||||
</button>
|
||||
<button
|
||||
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"
|
||||
disabled={createLoading}
|
||||
>
|
||||
{createLoading ? 'Creating...' : 'Create Requirement'}
|
||||
{createLoading ? t('createModal.creating') : t('createModal.createButton')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user