From fdf1d1b93a5c789f371bb3802add2b5f8d934b88 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Tue, 11 Mar 2025 11:09:01 -0400 Subject: [PATCH 1/7] remove icons --- .../(dashboard)/(home)/components/FrameworkProgress.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/(home)/components/FrameworkProgress.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/(home)/components/FrameworkProgress.tsx index 786cad9aa3..aa763b5b68 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/(home)/components/FrameworkProgress.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/(home)/components/FrameworkProgress.tsx @@ -32,12 +32,10 @@ export function FrameworkProgress({ frameworks }: Props) { const CircleProgress = ({ percentage, label, - icon, href, }: { percentage: number; label: string; - icon: React.ReactNode; href: string; }) => (
- {icon} {label}
@@ -155,19 +152,16 @@ export function FrameworkProgress({ frameworks }: Props) { } href="/policies/all" /> } href="/evidence/list" /> } href="/tests" /> From 572a71c91dc73ac63e43fdaeb6ed49ff5b58a29b Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Tue, 11 Mar 2025 11:20:46 -0400 Subject: [PATCH 2/7] fix imports --- .../(app)/(dashboard)/(home)/components/FrameworkProgress.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/(home)/components/FrameworkProgress.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/(home)/components/FrameworkProgress.tsx index aa763b5b68..60f23383e7 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/(home)/components/FrameworkProgress.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/(home)/components/FrameworkProgress.tsx @@ -1,5 +1,6 @@ "use client"; +import { useComplianceScores } from "@/hooks/use-compliance-scores"; import { useI18n } from "@/locales/client"; import type { Framework, @@ -8,9 +9,8 @@ import type { } from "@bubba/db"; import { Button } from "@bubba/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card"; -import { FileStack, FileText, CheckCircle, Cloud } from "lucide-react"; +import { FileStack } from "lucide-react"; import Link from "next/link"; -import { useComplianceScores } from "@/hooks/use-compliance-scores"; interface Props { frameworks: (OrganizationFramework & { From 6f273e8607532b66ccd4ab5e0073b2cb9c936b7a Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Tue, 11 Mar 2025 11:20:58 -0400 Subject: [PATCH 3/7] update employee table translations --- apps/app/src/locales/en.ts | 1856 ++++++++++++++++++------------------ 1 file changed, 928 insertions(+), 928 deletions(-) diff --git a/apps/app/src/locales/en.ts b/apps/app/src/locales/en.ts index f4011ec632..8587681a3a 100644 --- a/apps/app/src/locales/en.ts +++ b/apps/app/src/locales/en.ts @@ -1,931 +1,931 @@ export default { - language: { - title: "Languages", - description: "Change the language used in the user interface.", - placeholder: "Select language", - }, - languages: { - en: "English", - es: "Spanish", - fr: "French", - no: "Norwegian", - pt: "Portuguese", - }, - common: { - frequency: { - daily: "Daily", - weekly: "Weekly", - monthly: "Monthly", - quarterly: "Quarterly", - yearly: "Yearly", - }, - notifications: { - inbox: "Inbox", - archive: "Archive", - archive_all: "Archive all", - no_notifications: "No new notifications", - }, - actions: { - save: "Save", - edit: "Edit", - delete: "Delete", - cancel: "Cancel", - clear: "Clear", - create: "Create", - addNew: "Add New", - send: "Send", - return: "Return", - success: "Success", - error: "Error", - next: "Next", - complete: "Complete", - }, - assignee: { - label: "Assignee", - placeholder: "Select assignee", - }, - date: { - pick: "Pick a date", - due_date: "Due Date", - }, - status: { - open: "Open", - pending: "Pending", - closed: "Closed", - archived: "Archived", - compliant: "Compliant", - non_compliant: "Non Compliant", - not_started: "Not Started", - in_progress: "In Progress", - published: "Published", - needs_review: "Needs Review", - draft: "Draft", - not_assessed: "Not Assessed", - assessed: "Assessed", - active: "Active", - inactive: "Inactive", - title: "Status", - }, - filters: { - clear: "Clear filters", - search: "Search...", - status: "Status", - department: "Department", - owner: { - label: "Assignee", - placeholder: "Filter by assignee", - }, - }, - table: { - title: "Title", - status: "Status", - assigned_to: "Assigned To", - due_date: "Due Date", - last_updated: "Last Updated", - no_results: "No results found", - }, - empty_states: { - no_results: { - title: "No results found", - title_tasks: "No tasks found", - title_risks: "No risks found", - description: "Try another search, or adjusting the filters", - description_filters: "Try another search, or adjusting the filters", - description_no_tasks: "Create a task to get started", - description_no_risks: "Create a risk to get started", - }, - no_items: { - title: "No items found", - description: "Try adjusting your search or filters", - }, - }, - pagination: { - of: "of", - items_per_page: "Items per page", - rows_per_page: "Rows per page", - page_x_of_y: "Page {{current}} of {{total}}", - go_to_first_page: "Go to first page", - go_to_previous_page: "Go to previous page", - go_to_next_page: "Go to next page", - go_to_last_page: "Go to last page", - }, - comments: { - title: "Comments", - description: "Add a comment using the form below.", - add: "New Comment", - new: "New Comment", - save: "Save Comment", - success: "Comment added successfully", - error: "Failed to add comment", - placeholder: "Write your comment here...", - empty: { - title: "No comments yet", - description: "Be the first to add a comment", - }, - }, - upload: { - fileUpload: { - uploadingText: "Uploading...", - uploadingFile: "Uploading file...", - dropFileHere: "Drop file here", - dropFileHereAlt: "Drop file here", - releaseToUpload: "Release to upload", - addFiles: "Add Files", - uploadAdditionalEvidence: "Upload a file or document", - dragDropOrClick: "Drag and drop or click to browse", - dragDropOrClickToSelect: "Drag and drop or click to select file", - maxFileSize: "Max file size: {size}MB", - }, - fileUrl: { - additionalLinks: "Additional Links", - add: "Add", - linksAdded: "{count} link{s} added", - enterUrl: "Enter URL", - addAnotherLink: "Add Another Link", - saveLinks: "Save Links", - urlBadge: "URL", - copyLink: "Copy Link", - openLink: "Open Link", - deleteLink: "Delete Link", - }, - fileCard: { - preview: "Preview", - filePreview: "File Preview: {fileName}", - previewNotAvailable: "Preview not available for this file type", - openFile: "Open File", - deleteFile: "Delete File", - deleteFileConfirmTitle: "Delete File", - deleteFileConfirmDescription: - "This action cannot be undone. The file will be permanently deleted.", - }, - fileSection: { - filesUploaded: "{count} files uploaded", - }, - }, - attachments: { - title: "Attachments", - description: "Add a file by clicking 'Add Attachment'.", - upload: "Upload attachment", - upload_description: - "Upload an attachment or add a link to an external resource.", - drop: "Drop the files here", - drop_description: - "Drop files here or click to choose files from your device.", - drop_files_description: "Files can be up to ", - empty: { - title: "No attachments", - description: "Add a file by clicking 'Add Attachment'.", - }, - toasts: { - error: "Something went wrong, please try again.", - error_uploading_files: "Cannot upload more than 1 file at a time", - error_uploading_files_multiple: "Cannot upload more than 10 files", - error_no_files_selected: "No files selected", - error_file_rejected: "File {file} was rejected", - error_failed_to_upload_files: "Failed to upload files", - error_failed_to_upload_files_multiple: "Failed to upload files", - error_failed_to_upload_files_single: "Failed to upload file", - success_uploading_files: "Files uploaded successfully", - success_uploading_files_multiple: "Files uploaded successfully", - success_uploading_files_single: "File uploaded successfully", - success_uploading_files_target: "Files uploaded", - uploading_files: "Uploading {target}...", - remove_file: "Remove file", - }, - }, - edit: "Edit", - errors: { - unexpected_error: "An unexpected error occurred", - }, - description: "Description", - last_updated: "Last Updated", - }, - header: { - discord: { - button: "Join us on Discord", - }, - feedback: { - button: "Feedback", - title: "Thank you for your feedback!", - description: "We will be back with you as soon as possible", - placeholder: "Ideas to improve this page or issues you are experiencing.", - success: "Thank you for your feedback!", - error: "Error sending feedback - try again?", - send: "Send Feedback", - }, - }, - not_found: { - title: "404 - Page not found", - description: "The page you are looking for does not exist.", - return: "Return to dashboard", - }, - theme: { - options: { - light: "Light", - dark: "Dark", - system: "System", - }, - }, - sidebar: { - overview: "Overview", - policies: "Policies", - risk: "Risk Management", - vendors: "Vendors", - integrations: "Integrations", - settings: "Settings", - evidence: "Evidence Tasks", - people: "People", - tests: "Cloud Tests", - }, - sub_pages: { - evidence: { - title: "Evidence", - list: "Evidence List", - overview: "Evidence Overview", - }, - risk: { - overview: "Risk Management", - register: "Risk Register", - risk_overview: "Risk Overview", - risk_comments: "Risk Comments", - tasks: { - task_overview: "Task Overview", - }, - }, - policies: { - all: "All Policies", - editor: "Policy Editor", - policy_details: "Policy Details", - }, - people: { - all: "People", - employee_details: "Employee Details", - }, - settings: { - members: "Team Members", - }, - frameworks: { - overview: "Frameworks", - }, - }, - auth: { - title: "Automate SOC 2, ISO 27001 and GDPR compliance with AI.", - description: - "Create a free account or log in with an existing account to continue.", - options: "More options", - google: "Continue with Google", - email: { - description: "Enter your email address to continue.", - placeholder: "Enter email address", - button: "Continue with email", - magic_link_sent: "Magic link sent", - magic_link_description: "Check your inbox for a magic link.", - magic_link_try_again: "Try again.", - success: "Email sent - check your inbox!", - error: "Error sending email - try again?", - }, - terms: - "By clicking continue, you acknowledge that you have read and agree to the Terms of Service and Privacy Policy.", - }, - onboarding: { - title: "Create an organization", - setup: "Setup", - description: "Tell us a bit about your organization.", - fields: { - fullName: { - label: "Your Name", - placeholder: "Your full name", - }, - name: { - label: "Organization Name", - placeholder: "Your organization name", - }, - subdomain: { - label: "Subdomain", - placeholder: "example", - }, - website: { - label: "Website", - placeholder: "Your organization website", - }, - }, - success: "Thanks, you're all set!", - error: "Something went wrong, please try again.", - check_availability: "Checking availability", - available: "Available", - unavailable: "Unavailable", - }, - overview: { - title: "Overview", - framework_chart: { - title: "Framework Progress", - }, - requirement_chart: { - title: "Compliance Status", - }, - }, - policies: { - dashboard: { - title: "Dashboard", - all: "All Policies", - policy_status: "Policy by Status", - policies_by_assignee: "Policies by Assignee", - policies_by_framework: "Policies by Framework", - sub_pages: { - overview: "Overview", - edit_policy: "Edit Policy", - }, - }, - overview: { - title: "Policy Overview", - form: { - update_policy: "Update Policy", - update_policy_description: "Update the policy title or description.", - update_policy_success: "Policy updated successfully", - update_policy_error: "Failed to update policy", - update_policy_title: "Policy Name", - review_frequency: "Review Frequency", - review_frequency_placeholder: "Select a review frequency", - review_date: "Review Date", - review_date_placeholder: "Select a review date", - required_to_sign: "Required to be signed by employees", - signature_required: "Require employees signature", - signature_not_required: "Do not ask employees to sign", - signature_requirement: "Signature Requirement", - signature_requirement_placeholder: "Select signature requirement", - }, - }, - table: { - name: "Policy Name", - statuses: { - draft: "Draft", - published: "Published", - archived: "Archived", - }, - filters: { - owner: { - label: "Assignee", - placeholder: "Filter by assignee", - }, - }, - }, - filters: { - search: "Search policies...", - all: "All Policies", - }, - status: { - draft: "Draft", - published: "Published", - needs_review: "Needs Review", - archived: "Archived", - }, - policies: "policies", - title: "Policies", - create_new: "Create New Policy", - search_placeholder: "Search policies...", - status_filter: "Filter by status", - all_statuses: "All statuses", - no_policies_title: "No policies yet", - no_policies_description: "Get started by creating your first policy", - create_first: "Create first policy", - no_description: "No description provided", - last_updated: "Last updated: {{date}}", - save: "Save", - saving: "Saving...", - saved_success: "Policy saved successfully", - saved_error: "Failed to save policy", - }, - evidence_tasks: { - evidence_tasks: "Evidence Tasks", - overview: "Overview", - }, - risk: { - risks: "risks", - overview: "Overview", - create: "Create New Risk", - vendor: { - title: "Vendor Management", - dashboard: { - title: "Vendor Dashboard", - overview: "Vendor Overview", - vendor_status: "Vendor Status", - vendor_category: "Vendor Categories", - vendors_by_assignee: "Vendors by Assignee", - inherent_risk_description: - "Initial risk level before any controls are applied", - residual_risk_description: - "Remaining risk level after controls are applied", - }, - register: { - title: "Vendor Register", - table: { - name: "Name", - category: "Category", - status: "Status", - owner: "Owner", - }, - }, - assessment: { - title: "Vendor Assessment", - update_success: "Vendor risk assessment updated successfully", - update_error: "Failed to update vendor risk assessment", - inherent_risk: "Inherent Risk", - residual_risk: "Residual Risk", - }, - form: { - vendor_details: "Vendor Details", - vendor_name: "Name", - vendor_name_placeholder: "Enter vendor name", - vendor_website: "Website", - vendor_website_placeholder: "Enter vendor website", - vendor_description: "Description", - vendor_description_placeholder: "Enter vendor description", - vendor_category: "Category", - vendor_category_placeholder: "Select category", - vendor_status: "Status", - vendor_status_placeholder: "Select status", - create_vendor_success: "Vendor created successfully", - create_vendor_error: "Failed to create vendor", - update_vendor: "Update Vendor", - update_vendor_success: "Vendor updated successfully", - update_vendor_error: "Failed to update vendor", - add_comment: "Add Comment", - }, - table: { - name: "Name", - category: "Category", - status: "Status", - owner: "Owner", - }, - filters: { - search_placeholder: "Search vendors...", - status_placeholder: "Filter by status", - category_placeholder: "Filter by category", - owner_placeholder: "Filter by owner", - }, - empty_states: { - no_vendors: { - title: "No vendors yet", - description: "Get started by creating your first vendor", - }, - no_results: { - title: "No results found", - description: "No vendors match your search", - description_with_filters: "Try adjusting your filters", - }, - }, - actions: { - create: "Create Vendor", - }, - status: { - not_assessed: "Not Assessed", - in_progress: "In Progress", - assessed: "Assessed", - }, - category: { - cloud: "Cloud", - infrastructure: "Infrastructure", - software_as_a_service: "Software as a Service", - finance: "Finance", - marketing: "Marketing", - sales: "Sales", - hr: "HR", - other: "Other", - }, - risk_level: { - low: "Low Risk", - medium: "Medium Risk", - high: "High Risk", - unknown: "Unknown Risk", - }, - }, - dashboard: { - title: "Dashboard", - overview: "Risk Overview", - risk_status: "Risk Status", - risks_by_department: "Risks by Department", - risks_by_assignee: "Risks by Assignee", - inherent_risk_description: - "Inherent risk is calculated as likelihood * impact. Calculated before any controls are applied.", - residual_risk_description: - "Residual risk is calculated as likelihood * impact. This is the risk level after controls are applied.", - risk_assessment_description: "Compare inherent and residual risk levels", - }, - register: { - title: "Risk Register", - table: { - risk: "Risk", - }, - empty: { - no_risks: { - title: "Create a risk to get started", - description: - "Track and score risks, create and assign mitigation tasks for your team, and manage your risk register all in one simple interface.", - }, - create_risk: "Create a risk", - }, - }, - metrics: { - probability: "Probability", - impact: "Impact", - inherentRisk: "Inherent Risk", - residualRisk: "Residual Risk", - }, - form: { - update_inherent_risk: "Save Inherent Risk", - update_inherent_risk_description: - "Update the inherent risk of the risk. This is the risk level before any controls are applied.", - update_inherent_risk_success: "Inherent risk updated successfully", - update_inherent_risk_error: "Failed to update inherent risk", - update_residual_risk: "Save Residual Risk", - update_residual_risk_description: - "Update the residual risk of the risk. This is the risk level after controls are applied.", - update_residual_risk_success: "Residual risk updated successfully", - update_residual_risk_error: "Failed to update residual risk", - update_risk: "Update Risk", - update_risk_description: "Update the risk title or description.", - update_risk_success: "Risk updated successfully", - update_risk_error: "Failed to update risk", - create_risk_success: "Risk created successfully", - create_risk_error: "Failed to create risk", - risk_details: "Risk Details", - risk_title: "Risk Title", - risk_title_description: "Enter a name for the risk", - risk_description: "Description", - risk_description_description: "Enter a description for the risk", - risk_category: "Category", - risk_category_placeholder: "Select a category", - risk_department: "Department", - risk_department_placeholder: "Select a department", - risk_status: "Risk Status", - risk_status_placeholder: "Select a risk status", - }, - tasks: { - title: "Tasks", - attachments: "Attachments", - overview: "Task Overview", - form: { - title: "Task Details", - task_title: "Task Title", - status: "Task Status", - status_placeholder: "Select a task status", - task_title_description: "Enter a name for the task", - description: "Description", - description_description: "Enter a description for the task", - due_date: "Due Date", - due_date_description: "Select the due date for the task", - success: "Task created successfully", - error: "Failed to create task", - }, - sheet: { - title: "Create Task", - update: "Update Task", - update_description: "Update the task title or description.", - }, - empty: { - description_create: - "Create a mitigation task for this risk, add a treatment plan, and assign it to a team member.", - }, - }, - }, - people: { - title: "People", - details: { - taskProgress: "Task Progress", - tasks: "Tasks", - noTasks: "No tasks assigned yet", - }, - description: "Manage your team members and their roles.", - filters: { - search: "Search people...", - role: "Filter by role", - }, - actions: { - invite: "Add Employee", - clear: "Clear filters", - }, - table: { - name: "Name", - email: "Email", - department: "Department", - externalId: "External ID", - }, - empty: { - no_employees: { - title: "No employees yet", - description: "Get started by inviting your first team member.", - }, - no_results: { - title: "No results found", - description: "No employees match your search", - description_with_filters: "Try adjusting your filters", - }, - }, - invite: { - title: "Add Employee", - description: "Add an employee to your organization.", - email: { - label: "Email address", - placeholder: "Enter email address", - }, - role: { - label: "Role", - placeholder: "Select a role", - }, - name: { - label: "Name", - placeholder: "Enter name", - }, - department: { - label: "Department", - placeholder: "Select a department", - }, - submit: "Add Employee", - success: "Employee added successfully", - error: "Failed to add employee", - }, - }, - settings: { - general: { - title: "General", - org_name: "Organization name", - org_name_description: - "This is your organizations visible name. You should use the legal name of your organization.", - org_name_tip: "Please use 32 characters at maximum.", - org_website: "Organization Website", - org_website_description: - "This is your organization's official website URL. Make sure to include the full URL with https://.", - org_website_tip: "Please enter a valid URL including https://", - org_website_error: "Error updating organization website", - org_website_updated: "Organization website updated", - org_delete: "Delete organization", - org_delete_description: - "Permanently remove your organization and all of its contents from the Comp AI platform. This action is not reversible - please continue with caution.", - org_delete_alert_title: "Are you absolutely sure?", - org_delete_alert_description: - "This action cannot be undone. This will permanently delete your organization and remove your data from our servers.", - org_delete_error: "Error deleting organization", - org_delete_success: "Organization deleted", - org_name_updated: "Organization name updated", - org_name_error: "Error updating organization name", - save_button: "Save", - delete_button: "Delete", - delete_confirm: "DELETE", - delete_confirm_tip: "Type DELETE to confirm.", - cancel_button: "Cancel", - }, - members: { - title: "Members", - }, - api_keys: { - title: "API Keys", - description: - "Manage API keys for programmatic access to your organization's data.", - list_title: "API Keys", - list_description: - "API keys allow secure access to your organization's data via our API.", - create: "New API Key", - create_title: "New API Key", - create_description: - "Create a new API key for programmatic access to your organization's data.", - created_title: "API Key Created", - created_description: - "Your API key has been created. Make sure to copy it now as you won't be able to see it again.", - name: "Name", - name_label: "Name", - name_placeholder: "Enter a name for this API key", - expiration: "Expiration", - expiration_placeholder: "Select expiration", - expires_label: "Expires", - expires_placeholder: "Select expiration", - expires_30days: "30 days", - expires_90days: "90 days", - expires_1year: "1 year", - expires_never: "Never", - thirty_days: "30 days", - ninety_days: "90 days", - one_year: "1 year", - your_key: "Your API Key", - api_key: "API Key", - save_warning: - "This key will only be shown once. Make sure to copy it now.", - copied: "API key copied to clipboard", - close_confirm: - "Are you sure you want to close? You won't be able to see this API key again.", - revoke_confirm: - "Are you sure you want to revoke this API key? This action cannot be undone.", - revoke_title: "Revoke API Key", - revoke: "Revoke", - created: "Created", - expires: "Expires", - last_used: "Last Used", - actions: "Actions", - never: "Never", - never_used: "Never used", - no_keys: "No API keys found. Create one to get started.", - security_note: - "API keys provide full access to your organization's data. Keep them secure and rotate them regularly.", - fetch_error: "Failed to fetch API keys", - create_error: "Failed to create API key", - revoked_success: "API key revoked successfully", - revoked_error: "Failed to revoke API key", - done: "Done", - }, - billing: { - title: "Billing", - }, - }, - tests: { - name: "Cloud Tests", - title: "Cloud Tests", - actions: { - create: "Add Cloud Test", - clear: "Clear filters", - refresh: "Refresh", - }, - empty: { - no_tests: { - title: "No cloud tests yet", - description: "Get started by creating your first cloud test.", - }, - no_results: { - title: "No results found", - description: "No tests match your search", - description_with_filters: "Try adjusting your filters", - }, - }, - filters: { - search: "Search tests...", - role: "Filter by vendor", - }, - register: { - title: "Add Cloud Test", - description: "Configure a new cloud compliance test.", - submit: "Create Test", - success: "Test created successfully", - invalid_json: "Invalid JSON configuration provided", + language: { + title: "Languages", + description: "Change the language used in the user interface.", + placeholder: "Select language", + }, + languages: { + en: "English", + es: "Spanish", + fr: "French", + no: "Norwegian", + pt: "Portuguese", + }, + common: { + frequency: { + daily: "Daily", + weekly: "Weekly", + monthly: "Monthly", + quarterly: "Quarterly", + yearly: "Yearly", + }, + notifications: { + inbox: "Inbox", + archive: "Archive", + archive_all: "Archive all", + no_notifications: "No new notifications", + }, + actions: { + save: "Save", + edit: "Edit", + delete: "Delete", + cancel: "Cancel", + clear: "Clear", + create: "Create", + addNew: "Add New", + send: "Send", + return: "Return", + success: "Success", + error: "Error", + next: "Next", + complete: "Complete", + }, + assignee: { + label: "Assignee", + placeholder: "Select assignee", + }, + date: { + pick: "Pick a date", + due_date: "Due Date", + }, + status: { + open: "Open", + pending: "Pending", + closed: "Closed", + archived: "Archived", + compliant: "Compliant", + non_compliant: "Non Compliant", + not_started: "Not Started", + in_progress: "In Progress", + published: "Published", + needs_review: "Needs Review", + draft: "Draft", + not_assessed: "Not Assessed", + assessed: "Assessed", + active: "Active", + inactive: "Inactive", + title: "Status", + }, + filters: { + clear: "Clear filters", + search: "Search...", + status: "Status", + department: "Department", + owner: { + label: "Assignee", + placeholder: "Filter by assignee", + }, + }, + table: { + title: "Title", + status: "Status", + assigned_to: "Assigned To", + due_date: "Due Date", + last_updated: "Last Updated", + no_results: "No results found", + }, + empty_states: { + no_results: { + title: "No results found", + title_tasks: "No tasks found", + title_risks: "No risks found", + description: "Try another search, or adjusting the filters", + description_filters: "Try another search, or adjusting the filters", + description_no_tasks: "Create a task to get started", + description_no_risks: "Create a risk to get started", + }, + no_items: { + title: "No items found", + description: "Try adjusting your search or filters", + }, + }, + pagination: { + of: "of", + items_per_page: "Items per page", + rows_per_page: "Rows per page", + page_x_of_y: "Page {{current}} of {{total}}", + go_to_first_page: "Go to first page", + go_to_previous_page: "Go to previous page", + go_to_next_page: "Go to next page", + go_to_last_page: "Go to last page", + }, + comments: { + title: "Comments", + description: "Add a comment using the form below.", + add: "New Comment", + new: "New Comment", + save: "Save Comment", + success: "Comment added successfully", + error: "Failed to add comment", + placeholder: "Write your comment here...", + empty: { + title: "No comments yet", + description: "Be the first to add a comment", + }, + }, + upload: { + fileUpload: { + uploadingText: "Uploading...", + uploadingFile: "Uploading file...", + dropFileHere: "Drop file here", + dropFileHereAlt: "Drop file here", + releaseToUpload: "Release to upload", + addFiles: "Add Files", + uploadAdditionalEvidence: "Upload a file or document", + dragDropOrClick: "Drag and drop or click to browse", + dragDropOrClickToSelect: "Drag and drop or click to select file", + maxFileSize: "Max file size: {size}MB", + }, + fileUrl: { + additionalLinks: "Additional Links", + add: "Add", + linksAdded: "{count} link{s} added", + enterUrl: "Enter URL", + addAnotherLink: "Add Another Link", + saveLinks: "Save Links", + urlBadge: "URL", + copyLink: "Copy Link", + openLink: "Open Link", + deleteLink: "Delete Link", + }, + fileCard: { + preview: "Preview", + filePreview: "File Preview: {fileName}", + previewNotAvailable: "Preview not available for this file type", + openFile: "Open File", + deleteFile: "Delete File", + deleteFileConfirmTitle: "Delete File", + deleteFileConfirmDescription: + "This action cannot be undone. The file will be permanently deleted.", + }, + fileSection: { + filesUploaded: "{count} files uploaded", + }, + }, + attachments: { + title: "Attachments", + description: "Add a file by clicking 'Add Attachment'.", + upload: "Upload attachment", + upload_description: + "Upload an attachment or add a link to an external resource.", + drop: "Drop the files here", + drop_description: + "Drop files here or click to choose files from your device.", + drop_files_description: "Files can be up to ", + empty: { + title: "No attachments", + description: "Add a file by clicking 'Add Attachment'.", + }, + toasts: { + error: "Something went wrong, please try again.", + error_uploading_files: "Cannot upload more than 1 file at a time", + error_uploading_files_multiple: "Cannot upload more than 10 files", + error_no_files_selected: "No files selected", + error_file_rejected: "File {file} was rejected", + error_failed_to_upload_files: "Failed to upload files", + error_failed_to_upload_files_multiple: "Failed to upload files", + error_failed_to_upload_files_single: "Failed to upload file", + success_uploading_files: "Files uploaded successfully", + success_uploading_files_multiple: "Files uploaded successfully", + success_uploading_files_single: "File uploaded successfully", + success_uploading_files_target: "Files uploaded", + uploading_files: "Uploading {target}...", + remove_file: "Remove file", + }, + }, + edit: "Edit", + errors: { + unexpected_error: "An unexpected error occurred", + }, + description: "Description", + last_updated: "Last Updated", + }, + header: { + discord: { + button: "Join us on Discord", + }, + feedback: { + button: "Feedback", + title: "Thank you for your feedback!", + description: "We will be back with you as soon as possible", + placeholder: "Ideas to improve this page or issues you are experiencing.", + success: "Thank you for your feedback!", + error: "Error sending feedback - try again?", + send: "Send Feedback", + }, + }, + not_found: { + title: "404 - Page not found", + description: "The page you are looking for does not exist.", + return: "Return to dashboard", + }, + theme: { + options: { + light: "Light", + dark: "Dark", + system: "System", + }, + }, + sidebar: { + overview: "Overview", + policies: "Policies", + risk: "Risk Management", + vendors: "Vendors", + integrations: "Integrations", + settings: "Settings", + evidence: "Evidence Tasks", + people: "People", + tests: "Cloud Tests", + }, + sub_pages: { + evidence: { + title: "Evidence", + list: "Evidence List", + overview: "Evidence Overview", + }, + risk: { + overview: "Risk Management", + register: "Risk Register", + risk_overview: "Risk Overview", + risk_comments: "Risk Comments", + tasks: { + task_overview: "Task Overview", + }, + }, + policies: { + all: "All Policies", + editor: "Policy Editor", + policy_details: "Policy Details", + }, + people: { + all: "People", + employee_details: "Employee Details", + }, + settings: { + members: "Team Members", + }, + frameworks: { + overview: "Frameworks", + }, + }, + auth: { + title: "Automate SOC 2, ISO 27001 and GDPR compliance with AI.", + description: + "Create a free account or log in with an existing account to continue.", + options: "More options", + google: "Continue with Google", + email: { + description: "Enter your email address to continue.", + placeholder: "Enter email address", + button: "Continue with email", + magic_link_sent: "Magic link sent", + magic_link_description: "Check your inbox for a magic link.", + magic_link_try_again: "Try again.", + success: "Email sent - check your inbox!", + error: "Error sending email - try again?", + }, + terms: + "By clicking continue, you acknowledge that you have read and agree to the Terms of Service and Privacy Policy.", + }, + onboarding: { + title: "Create an organization", + setup: "Setup", + description: "Tell us a bit about your organization.", + fields: { + fullName: { + label: "Your Name", + placeholder: "Your full name", + }, + name: { + label: "Organization Name", + placeholder: "Your organization name", + }, + subdomain: { + label: "Subdomain", + placeholder: "example", + }, + website: { + label: "Website", + placeholder: "Your organization website", + }, + }, + success: "Thanks, you're all set!", + error: "Something went wrong, please try again.", + check_availability: "Checking availability", + available: "Available", + unavailable: "Unavailable", + }, + overview: { + title: "Overview", + framework_chart: { + title: "Framework Progress", + }, + requirement_chart: { + title: "Compliance Status", + }, + }, + policies: { + dashboard: { + title: "Dashboard", + all: "All Policies", + policy_status: "Policy by Status", + policies_by_assignee: "Policies by Assignee", + policies_by_framework: "Policies by Framework", + sub_pages: { + overview: "Overview", + edit_policy: "Edit Policy", + }, + }, + overview: { + title: "Policy Overview", + form: { + update_policy: "Update Policy", + update_policy_description: "Update the policy title or description.", + update_policy_success: "Policy updated successfully", + update_policy_error: "Failed to update policy", + update_policy_title: "Policy Name", + review_frequency: "Review Frequency", + review_frequency_placeholder: "Select a review frequency", + review_date: "Review Date", + review_date_placeholder: "Select a review date", + required_to_sign: "Required to be signed by employees", + signature_required: "Require employees signature", + signature_not_required: "Do not ask employees to sign", + signature_requirement: "Signature Requirement", + signature_requirement_placeholder: "Select signature requirement", + }, + }, + table: { + name: "Policy Name", + statuses: { + draft: "Draft", + published: "Published", + archived: "Archived", + }, + filters: { + owner: { + label: "Assignee", + placeholder: "Filter by assignee", + }, + }, + }, + filters: { + search: "Search policies...", + all: "All Policies", + }, + status: { + draft: "Draft", + published: "Published", + needs_review: "Needs Review", + archived: "Archived", + }, + policies: "policies", + title: "Policies", + create_new: "Create New Policy", + search_placeholder: "Search policies...", + status_filter: "Filter by status", + all_statuses: "All statuses", + no_policies_title: "No policies yet", + no_policies_description: "Get started by creating your first policy", + create_first: "Create first policy", + no_description: "No description provided", + last_updated: "Last updated: {{date}}", + save: "Save", + saving: "Saving...", + saved_success: "Policy saved successfully", + saved_error: "Failed to save policy", + }, + evidence_tasks: { + evidence_tasks: "Evidence Tasks", + overview: "Overview", + }, + risk: { + risks: "risks", + overview: "Overview", + create: "Create New Risk", + vendor: { + title: "Vendor Management", + dashboard: { + title: "Vendor Dashboard", + overview: "Vendor Overview", + vendor_status: "Vendor Status", + vendor_category: "Vendor Categories", + vendors_by_assignee: "Vendors by Assignee", + inherent_risk_description: + "Initial risk level before any controls are applied", + residual_risk_description: + "Remaining risk level after controls are applied", + }, + register: { + title: "Vendor Register", + table: { + name: "Name", + category: "Category", + status: "Status", + owner: "Owner", + }, + }, + assessment: { + title: "Vendor Assessment", + update_success: "Vendor risk assessment updated successfully", + update_error: "Failed to update vendor risk assessment", + inherent_risk: "Inherent Risk", + residual_risk: "Residual Risk", + }, + form: { + vendor_details: "Vendor Details", + vendor_name: "Name", + vendor_name_placeholder: "Enter vendor name", + vendor_website: "Website", + vendor_website_placeholder: "Enter vendor website", + vendor_description: "Description", + vendor_description_placeholder: "Enter vendor description", + vendor_category: "Category", + vendor_category_placeholder: "Select category", + vendor_status: "Status", + vendor_status_placeholder: "Select status", + create_vendor_success: "Vendor created successfully", + create_vendor_error: "Failed to create vendor", + update_vendor: "Update Vendor", + update_vendor_success: "Vendor updated successfully", + update_vendor_error: "Failed to update vendor", + add_comment: "Add Comment", + }, + table: { + name: "Name", + category: "Category", + status: "Status", + owner: "Owner", + }, + filters: { + search_placeholder: "Search vendors...", + status_placeholder: "Filter by status", + category_placeholder: "Filter by category", + owner_placeholder: "Filter by owner", + }, + empty_states: { + no_vendors: { + title: "No vendors yet", + description: "Get started by creating your first vendor", + }, + no_results: { + title: "No results found", + description: "No vendors match your search", + description_with_filters: "Try adjusting your filters", + }, + }, + actions: { + create: "Create Vendor", + }, + status: { + not_assessed: "Not Assessed", + in_progress: "In Progress", + assessed: "Assessed", + }, + category: { + cloud: "Cloud", + infrastructure: "Infrastructure", + software_as_a_service: "Software as a Service", + finance: "Finance", + marketing: "Marketing", + sales: "Sales", + hr: "HR", + other: "Other", + }, + risk_level: { + low: "Low Risk", + medium: "Medium Risk", + high: "High Risk", + unknown: "Unknown Risk", + }, + }, + dashboard: { + title: "Dashboard", + overview: "Risk Overview", + risk_status: "Risk Status", + risks_by_department: "Risks by Department", + risks_by_assignee: "Risks by Assignee", + inherent_risk_description: + "Inherent risk is calculated as likelihood * impact. Calculated before any controls are applied.", + residual_risk_description: + "Residual risk is calculated as likelihood * impact. This is the risk level after controls are applied.", + risk_assessment_description: "Compare inherent and residual risk levels", + }, + register: { + title: "Risk Register", + table: { + risk: "Risk", + }, + empty: { + no_risks: { + title: "Create a risk to get started", + description: + "Track and score risks, create and assign mitigation tasks for your team, and manage your risk register all in one simple interface.", + }, + create_risk: "Create a risk", + }, + }, + metrics: { + probability: "Probability", + impact: "Impact", + inherentRisk: "Inherent Risk", + residualRisk: "Residual Risk", + }, + form: { + update_inherent_risk: "Save Inherent Risk", + update_inherent_risk_description: + "Update the inherent risk of the risk. This is the risk level before any controls are applied.", + update_inherent_risk_success: "Inherent risk updated successfully", + update_inherent_risk_error: "Failed to update inherent risk", + update_residual_risk: "Save Residual Risk", + update_residual_risk_description: + "Update the residual risk of the risk. This is the risk level after controls are applied.", + update_residual_risk_success: "Residual risk updated successfully", + update_residual_risk_error: "Failed to update residual risk", + update_risk: "Update Risk", + update_risk_description: "Update the risk title or description.", + update_risk_success: "Risk updated successfully", + update_risk_error: "Failed to update risk", + create_risk_success: "Risk created successfully", + create_risk_error: "Failed to create risk", + risk_details: "Risk Details", + risk_title: "Risk Title", + risk_title_description: "Enter a name for the risk", + risk_description: "Description", + risk_description_description: "Enter a description for the risk", + risk_category: "Category", + risk_category_placeholder: "Select a category", + risk_department: "Department", + risk_department_placeholder: "Select a department", + risk_status: "Risk Status", + risk_status_placeholder: "Select a risk status", + }, + tasks: { + title: "Tasks", + attachments: "Attachments", + overview: "Task Overview", + form: { + title: "Task Details", + task_title: "Task Title", + status: "Task Status", + status_placeholder: "Select a task status", + task_title_description: "Enter a name for the task", + description: "Description", + description_description: "Enter a description for the task", + due_date: "Due Date", + due_date_description: "Select the due date for the task", + success: "Task created successfully", + error: "Failed to create task", + }, + sheet: { + title: "Create Task", + update: "Update Task", + update_description: "Update the task title or description.", + }, + empty: { + description_create: + "Create a mitigation task for this risk, add a treatment plan, and assign it to a team member.", + }, + }, + }, + people: { + title: "People", + details: { + taskProgress: "Task Progress", + tasks: "Tasks", + noTasks: "No tasks assigned yet", + }, + description: "Manage your team members and their roles.", + filters: { + search: "Search people...", + role: "Filter by role", + }, + actions: { + invite: "Add Employee", + clear: "Clear filters", + }, + table: { + name: "Employee Name", + email: "Email", + department: "Department", + externalId: "External ID", + }, + empty: { + no_employees: { + title: "No employees yet", + description: "Get started by inviting your first team member.", + }, + no_results: { + title: "No results found", + description: "No employees match your search", + description_with_filters: "Try adjusting your filters", + }, + }, + invite: { + title: "Add Employee", + description: "Add an employee to your organization.", + email: { + label: "Email address", + placeholder: "Enter email address", + }, + role: { + label: "Role", + placeholder: "Select a role", + }, + name: { + label: "Name", + placeholder: "Enter name", + }, + department: { + label: "Department", + placeholder: "Select a department", + }, + submit: "Add Employee", + success: "Employee added successfully", + error: "Failed to add employee", + }, + }, + settings: { + general: { + title: "General", + org_name: "Organization name", + org_name_description: + "This is your organizations visible name. You should use the legal name of your organization.", + org_name_tip: "Please use 32 characters at maximum.", + org_website: "Organization Website", + org_website_description: + "This is your organization's official website URL. Make sure to include the full URL with https://.", + org_website_tip: "Please enter a valid URL including https://", + org_website_error: "Error updating organization website", + org_website_updated: "Organization website updated", + org_delete: "Delete organization", + org_delete_description: + "Permanently remove your organization and all of its contents from the Comp AI platform. This action is not reversible - please continue with caution.", + org_delete_alert_title: "Are you absolutely sure?", + org_delete_alert_description: + "This action cannot be undone. This will permanently delete your organization and remove your data from our servers.", + org_delete_error: "Error deleting organization", + org_delete_success: "Organization deleted", + org_name_updated: "Organization name updated", + org_name_error: "Error updating organization name", + save_button: "Save", + delete_button: "Delete", + delete_confirm: "DELETE", + delete_confirm_tip: "Type DELETE to confirm.", + cancel_button: "Cancel", + }, + members: { + title: "Members", + }, + api_keys: { + title: "API Keys", + description: + "Manage API keys for programmatic access to your organization's data.", + list_title: "API Keys", + list_description: + "API keys allow secure access to your organization's data via our API.", + create: "New API Key", + create_title: "New API Key", + create_description: + "Create a new API key for programmatic access to your organization's data.", + created_title: "API Key Created", + created_description: + "Your API key has been created. Make sure to copy it now as you won't be able to see it again.", + name: "Name", + name_label: "Name", + name_placeholder: "Enter a name for this API key", + expiration: "Expiration", + expiration_placeholder: "Select expiration", + expires_label: "Expires", + expires_placeholder: "Select expiration", + expires_30days: "30 days", + expires_90days: "90 days", + expires_1year: "1 year", + expires_never: "Never", + thirty_days: "30 days", + ninety_days: "90 days", + one_year: "1 year", + your_key: "Your API Key", + api_key: "API Key", + save_warning: + "This key will only be shown once. Make sure to copy it now.", + copied: "API key copied to clipboard", + close_confirm: + "Are you sure you want to close? You won't be able to see this API key again.", + revoke_confirm: + "Are you sure you want to revoke this API key? This action cannot be undone.", + revoke_title: "Revoke API Key", + revoke: "Revoke", + created: "Created", + expires: "Expires", + last_used: "Last Used", + actions: "Actions", + never: "Never", + never_used: "Never used", + no_keys: "No API keys found. Create one to get started.", + security_note: + "API keys provide full access to your organization's data. Keep them secure and rotate them regularly.", + fetch_error: "Failed to fetch API keys", + create_error: "Failed to create API key", + revoked_success: "API key revoked successfully", + revoked_error: "Failed to revoke API key", + done: "Done", + }, + billing: { + title: "Billing", + }, + }, + tests: { + name: "Cloud Tests", + title: "Cloud Tests", + actions: { + create: "Add Cloud Test", + clear: "Clear filters", + refresh: "Refresh", + }, + empty: { + no_tests: { + title: "No cloud tests yet", + description: "Get started by creating your first cloud test.", + }, + no_results: { + title: "No results found", + description: "No tests match your search", + description_with_filters: "Try adjusting your filters", + }, + }, + filters: { + search: "Search tests...", + role: "Filter by vendor", + }, + register: { + title: "Add Cloud Test", + description: "Configure a new cloud compliance test.", + submit: "Create Test", + success: "Test created successfully", + invalid_json: "Invalid JSON configuration provided", - title_field: { - label: "Test Title", - placeholder: "Enter test title", - }, - description_field: { - label: "Description", - placeholder: "Enter test description", - }, - provider: { - label: "Cloud Provider", - placeholder: "Select cloud provider", - }, - config: { - label: "Test Configuration", - placeholder: "Enter JSON configuration for the test", - }, - auth_config: { - label: "Authentication Configuration", - placeholder: "Enter JSON authentication configuration", - }, - }, - table: { - title: "Title", - provider: "Provider", - status: "Status", - severity: "Severity", - result: "Result", - createdAt: "Created At", - assignedUser: "Assigned User", - assignedUserEmpty: "Not Assigned", - no_results: "No results found", - }, - }, - user_menu: { - theme: "Theme", - language: "Language", - sign_out: "Sign out", - account: "Account", - support: "Support", - settings: "Settings", - teams: "Teams", - }, - frameworks: { - title: "Frameworks", - overview: { - error: "Failed to load frameworks", - loading: "Loading frameworks...", - empty: { - title: "No frameworks selected", - description: - "Select frameworks to get started with your compliance journey", - }, - progress: { - title: "Framework Progress", - empty: { - title: "No frameworks yet", - description: - "Get started by adding a compliance framework to track your progress", - action: "Add Framework", - }, - }, - grid: { - welcome: { - title: "Welcome to Comp AI", - description: - "Get started by selecting the compliance frameworks you would like to implement. We'll help you manage and track your compliance journey across multiple standards.", - action: "Get Started", - }, - title: "Select Frameworks", - version: "Version", - actions: { - clear: "Clear", - confirm: "Confirm Selection", - }, - }, - }, - controls: { - title: "Controls", - description: "Review and manage compliance controls", - table: { - status: "Status", - control: "Control", - artifacts: "Artifacts", - actions: "Actions", - }, - statuses: { - completed: "Completed", - in_progress: "In Progress", - not_started: "Not Started", - }, - }, - }, - errors: { - unexpected: "Something went wrong, please try again", - }, - editor: { - ai: { - thinking: "AI is thinking", - thinking_spinner: "AI is thinking", - edit_or_generate: "Edit or generate...", - tell_ai_what_to_do_next: "Tell AI what to do next", - request_limit_reached: "You have reached your request limit for the day.", - }, - ai_selector: { - improve: "Improve writing", - fix: "Fix grammar", - shorter: "Make shorter", - longer: "Make longer", - continue: "Continue writing", - replace: "Replace selection", - insert: "Insert below", - discard: "Discard", - }, - }, - evidence: { - title: "Evidence", - list: "All Evidence", - overview: "Evidence Overview", - edit: "Uploaded Evidence", - dashboard: { - layout: "Dashboard", - layout_back_button: "Back", - title: "Evidence Dashboard", - by_department: "By Department", - by_assignee: "By Assignee", - by_framework: "By Framework", - }, - items: "items", - status: { - up_to_date: "Up to Date", - needs_review: "Needs Review", - draft: "Draft", - empty: "Empty", - }, - departments: { - none: "Uncategorized", - admin: "Administration", - gov: "Governance", - hr: "Human Resources", - it: "Information Technology", - itsm: "IT Service Management", - qms: "Quality Management", - }, - details: { - review_section: "Review Information", - content: "Evidence Content", - }, - }, - vendors: { - title: "Vendors", - register: { - title: "Vendor Register", - }, - dashboard: { - title: "Vendor Overview", - }, - }, - dashboard: { - risk_status: "Risk Status", - risks_by_department: "Risks by Department", - vendor_status: "Vendor Status", - vendors_by_category: "Vendors by Category", - }, + title_field: { + label: "Test Title", + placeholder: "Enter test title", + }, + description_field: { + label: "Description", + placeholder: "Enter test description", + }, + provider: { + label: "Cloud Provider", + placeholder: "Select cloud provider", + }, + config: { + label: "Test Configuration", + placeholder: "Enter JSON configuration for the test", + }, + auth_config: { + label: "Authentication Configuration", + placeholder: "Enter JSON authentication configuration", + }, + }, + table: { + title: "Title", + provider: "Provider", + status: "Status", + severity: "Severity", + result: "Result", + createdAt: "Created At", + assignedUser: "Assigned User", + assignedUserEmpty: "Not Assigned", + no_results: "No results found", + }, + }, + user_menu: { + theme: "Theme", + language: "Language", + sign_out: "Sign out", + account: "Account", + support: "Support", + settings: "Settings", + teams: "Teams", + }, + frameworks: { + title: "Frameworks", + overview: { + error: "Failed to load frameworks", + loading: "Loading frameworks...", + empty: { + title: "No frameworks selected", + description: + "Select frameworks to get started with your compliance journey", + }, + progress: { + title: "Framework Progress", + empty: { + title: "No frameworks yet", + description: + "Get started by adding a compliance framework to track your progress", + action: "Add Framework", + }, + }, + grid: { + welcome: { + title: "Welcome to Comp AI", + description: + "Get started by selecting the compliance frameworks you would like to implement. We'll help you manage and track your compliance journey across multiple standards.", + action: "Get Started", + }, + title: "Select Frameworks", + version: "Version", + actions: { + clear: "Clear", + confirm: "Confirm Selection", + }, + }, + }, + controls: { + title: "Controls", + description: "Review and manage compliance controls", + table: { + status: "Status", + control: "Control", + artifacts: "Artifacts", + actions: "Actions", + }, + statuses: { + completed: "Completed", + in_progress: "In Progress", + not_started: "Not Started", + }, + }, + }, + errors: { + unexpected: "Something went wrong, please try again", + }, + editor: { + ai: { + thinking: "AI is thinking", + thinking_spinner: "AI is thinking", + edit_or_generate: "Edit or generate...", + tell_ai_what_to_do_next: "Tell AI what to do next", + request_limit_reached: "You have reached your request limit for the day.", + }, + ai_selector: { + improve: "Improve writing", + fix: "Fix grammar", + shorter: "Make shorter", + longer: "Make longer", + continue: "Continue writing", + replace: "Replace selection", + insert: "Insert below", + discard: "Discard", + }, + }, + evidence: { + title: "Evidence", + list: "All Evidence", + overview: "Evidence Overview", + edit: "Uploaded Evidence", + dashboard: { + layout: "Dashboard", + layout_back_button: "Back", + title: "Evidence Dashboard", + by_department: "By Department", + by_assignee: "By Assignee", + by_framework: "By Framework", + }, + items: "items", + status: { + up_to_date: "Up to Date", + needs_review: "Needs Review", + draft: "Draft", + empty: "Empty", + }, + departments: { + none: "Uncategorized", + admin: "Administration", + gov: "Governance", + hr: "Human Resources", + it: "Information Technology", + itsm: "IT Service Management", + qms: "Quality Management", + }, + details: { + review_section: "Review Information", + content: "Evidence Content", + }, + }, + vendors: { + title: "Vendors", + register: { + title: "Vendor Register", + }, + dashboard: { + title: "Vendor Overview", + }, + }, + dashboard: { + risk_status: "Risk Status", + risks_by_department: "Risks by Department", + vendor_status: "Vendor Status", + vendors_by_category: "Vendors by Category", + }, } as const; From adde77074d1575628580ae261206d52852c0d070 Mon Sep 17 00:00:00 2001 From: Languine Bot Date: Tue, 11 Mar 2025 15:21:39 +0000 Subject: [PATCH 4/7] chore: (i18n) update translations using Languine.ai --- apps/app/languine.lock | 2 +- apps/app/src/locales/es.ts | 2 +- apps/app/src/locales/fr.ts | 2 +- apps/app/src/locales/no.ts | 2 +- apps/app/src/locales/pt.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/app/languine.lock b/apps/app/languine.lock index ec5411ce0e..a7739424fd 100644 --- a/apps/app/languine.lock +++ b/apps/app/languine.lock @@ -402,7 +402,7 @@ files: people.filters.role: 4a49167c423fab80b22db2b551813a81 people.actions.invite: 3aa4cd2c53d3d24b465398053e12fe65 people.actions.clear: 0b275442d6556cff30b75f37f1918899 - people.table.name: 49ee3087348e8d44e1feda1917443987 + people.table.name: 717231b5b2cf0543d46d6555f25c56b4 people.table.email: ce8ae9da5b7cd6c3df2929543a9af92d people.table.department: 1d17cb9923b99f823da9f5a16dc460e5 people.table.externalId: 01ddb49044c161b11fdb0c41adbeb3b3 diff --git a/apps/app/src/locales/es.ts b/apps/app/src/locales/es.ts index d78de610eb..fae9540058 100644 --- a/apps/app/src/locales/es.ts +++ b/apps/app/src/locales/es.ts @@ -763,7 +763,7 @@ export default { clear: "Limpiar filtros" }, table: { - name: "Nombre", + name: "Nombre del empleado", email: "Correo Electrónico", department: "Departamento", externalId: "ID Externo" diff --git a/apps/app/src/locales/fr.ts b/apps/app/src/locales/fr.ts index afdd85f673..94582af728 100644 --- a/apps/app/src/locales/fr.ts +++ b/apps/app/src/locales/fr.ts @@ -763,7 +763,7 @@ export default { clear: "Effacer les filtres" }, table: { - name: "Nom", + name: "Nom de l'employé", email: "Email", department: "Département", externalId: "ID externe" diff --git a/apps/app/src/locales/no.ts b/apps/app/src/locales/no.ts index 5a4f1bf4a0..a1bb43fa9e 100644 --- a/apps/app/src/locales/no.ts +++ b/apps/app/src/locales/no.ts @@ -763,7 +763,7 @@ export default { clear: "Fjern filtre" }, table: { - name: "Navn", + name: "Ansattnavn", email: "E-post", department: "Avdeling", externalId: "Ekstern ID" diff --git a/apps/app/src/locales/pt.ts b/apps/app/src/locales/pt.ts index 2786d9bf64..7c1dce53c6 100644 --- a/apps/app/src/locales/pt.ts +++ b/apps/app/src/locales/pt.ts @@ -763,7 +763,7 @@ export default { clear: "Limpar filtros" }, table: { - name: "Nome", + name: "Nome do Funcionário", email: "Email", department: "Departamento", externalId: "ID Externo" From 118890654123c1959de285dc78471ef17c301dea Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Tue, 11 Mar 2025 12:26:10 -0400 Subject: [PATCH 5/7] refactor(employees): modularize employee creation and Deel sync logic - Extract employee creation logic into reusable functions in `lib/db/employee.ts` - Implement `completeEmployeeCreation` to handle employee, portal user, and task creation - Enhance Deel sync to support both active and inactive employee processing - Improve error handling and logging during employee synchronization - Simplify `createEmployeeAction` by leveraging new database helper functions - Update `EmployeeDetails` component to display task status more clearly --- .../actions/people/create-employee-action.ts | 132 +---------- .../components/EmployeeDetails.tsx | 224 ++++++++---------- apps/app/src/lib/db/employee.ts | 185 +++++++++++++++ apps/app/src/trigger/deel/index.ts | 127 ++++++---- 4 files changed, 370 insertions(+), 298 deletions(-) create mode 100644 apps/app/src/lib/db/employee.ts diff --git a/apps/app/src/actions/people/create-employee-action.ts b/apps/app/src/actions/people/create-employee-action.ts index ab15b1074e..f6212e89e0 100644 --- a/apps/app/src/actions/people/create-employee-action.ts +++ b/apps/app/src/actions/people/create-employee-action.ts @@ -1,29 +1,10 @@ "use server"; -import { type Employee, db } from "@bubba/db"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; import { authActionClient } from "../safe-action"; import { createEmployeeSchema } from "../schema"; import type { ActionResponse } from "../types"; - -const DEFAULT_TASKS = [ - { - code: "POLICY-ACCEPT", - name: "Policy Acceptance", - description: "Review and accept company policies", - }, - { - code: "INSTALL-AGENT", - name: "Install Monitoring Agent", - description: - "Install and configure the security monitoring agent on your device", - }, - { - code: "DEVICE-SECURITY", - name: "Device Security", - description: "Complete device security checklist and configuration", - }, -] as const; +import { completeEmployeeCreation } from "@/lib/db/employee"; export const createEmployeeAction = authActionClient .schema(createEmployeeSchema) @@ -46,110 +27,14 @@ export const createEmployeeAction = authActionClient } try { - // First check if an employee exists (active or inactive) - const existingEmployee = await db.employee.findUnique({ - where: { - email_organizationId: { - email, - organizationId: user.organizationId, - }, - }, + const employee = await completeEmployeeCreation({ + name, + email, + department, + organizationId: user.organizationId, + externalEmployeeId, }); - let employee: Employee; - - if (existingEmployee) { - if (existingEmployee.isActive) { - return { - success: false, - error: - "An employee with this email already exists in your organization", - }; - } - - // Reactivate the existing employee - employee = await db.employee.update({ - where: { id: existingEmployee.id }, - data: { - name, - department, - isActive: true, - externalEmployeeId, - organizationId: user.organizationId, - updatedAt: new Date(), - }, - }); - } else { - employee = await db.employee.create({ - data: { - name, - email, - department, - organizationId: user.organizationId, - isActive: true, - externalEmployeeId, - }, - }); - } - - // Update or create portalUser - const portalUser = await db.portalUser.upsert({ - where: { email }, - create: { - id: employee.id, - name, - email, - organizationId: user.organizationId, - emailVerified: false, - createdAt: new Date(), - updatedAt: new Date(), - employees: { - connect: { - id: employee.id, - }, - }, - }, - update: { - updatedAt: new Date(), - name, - email, - organizationId: user.organizationId, - employees: { - connect: { - id: employee.id, - }, - }, - }, - }); - - // Create or get the required task definitions first and store their IDs - const requiredTasks = await Promise.all( - DEFAULT_TASKS.map(async (task) => { - return db.employeeRequiredTask.upsert({ - where: { code: task.code }, - create: { - code: task.code, - name: task.name, - description: task.description, - }, - update: {}, - }); - }), - ); - - // Now create the employee tasks using the actual task IDs - await Promise.all( - requiredTasks.map(async (task) => { - return db.employeeTask.create({ - data: { - employeeId: employee.id, - requiredTaskId: task.id, - status: "assigned", - }, - }); - }), - ); - return { success: true, data: employee, @@ -170,7 +55,8 @@ export const createEmployeeAction = authActionClient return { success: false, - error: "Failed to create employee", + error: + error instanceof Error ? error.message : "Failed to create employee", }; } }); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/components/EmployeeDetails.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/components/EmployeeDetails.tsx index b811ae9359..ca303647aa 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/components/EmployeeDetails.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/components/EmployeeDetails.tsx @@ -14,141 +14,119 @@ import { Label } from "@bubba/ui/label"; import { formatDate } from "@/utils/format"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@bubba/ui/tabs"; import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, } from "@bubba/ui/accordion"; interface EmployeeDetailsProps { - employeeId: string; + employeeId: string; } export function EmployeeDetails({ employeeId }: EmployeeDetailsProps) { - const t = useI18n(); - const { employee, isLoading, error } = useEmployeeDetails(employeeId); + const t = useI18n(); + const { employee, isLoading, error } = useEmployeeDetails(employeeId); - if (error) { - if (error.code === "NOT_FOUND") { - redirect("/people"); - } + if (error) { + if (error.code === "NOT_FOUND") { + redirect("/people"); + } - return ( -
- - - Error - - {error.message || "An unexpected error occurred"} - - -
- ); - } + return ( +
+ + + Error + + {error.message || "An unexpected error occurred"} + + +
+ ); + } - if (isLoading) { - return ( -
-
- - -
-
- - - - - -
- - -
-
-
-
-
- ); - } + if (isLoading) { + return ( +
+
+ + +
+
+ + + + + +
+ + +
+
+
+
+
+ ); + } - if (!employee) return null; + if (!employee) return null; - const tasks = employee.employeeTasks ?? []; + const tasks = employee.employeeTasks ?? []; - return ( -
-

- {employee.name} ({employee.isActive ? "Active" : "Inactive"}) -

+ return ( +
+

+ {employee.name} ({employee.isActive ? "Active" : "Inactive"}) +

- - -
-
- -

{employee.name}

-
-
- -

{employee.email}

-
-
- -

- {formatDate(employee.createdAt.toISOString(), "MMM d, yyyy")} -

-
-
- -

{employee.department}

-
-
-
-
+ + +
+
+ +

{employee.name}

+
+
+ +

{employee.email}

+
+
+ +

+ {formatDate(employee.createdAt.toISOString(), "MMM d, yyyy")} +

+
+
+ +

{employee.department}

+
+
+
+
- - - Tasks - Documents - Trainings - + + + Employee Tasks + + + {tasks.map((task) => { + const isCompleted = task.status === "completed"; - -
-
- {tasks.map((task) => ( - - - - {task.status === "assigned" && ( - - )} - {task.status === "completed" && ( - - )} - {task.requiredTask.name} -
- - -

{task.requiredTask.description}

-
- - - ))} -
-
- - - -
- -
-
- - -
- -
-
- -
- ); + return ( +
+

+ {isCompleted ? ( + + ) : ( + + )} + {task.requiredTask.name} +

+
+ ); + })} +
+
+
+ ); } diff --git a/apps/app/src/lib/db/employee.ts b/apps/app/src/lib/db/employee.ts new file mode 100644 index 0000000000..9a3a486698 --- /dev/null +++ b/apps/app/src/lib/db/employee.ts @@ -0,0 +1,185 @@ +import { type Employee, db } from "@bubba/db"; +import type { Departments } from "@prisma/client"; + +const DEFAULT_TASKS = [ + { + code: "POLICY-ACCEPT", + name: "Policy Acceptance", + description: "Review and accept company policies", + }, + { + code: "DEVICE-SECURITY", + name: "Device Security", + description: "Complete device security checklist and configuration", + }, +] as const; + +/** + * Find an existing employee by email and organization ID + */ +export async function findEmployeeByEmail( + email: string, + organizationId: string +): Promise { + return db.employee.findUnique({ + where: { + email_organizationId: { + email, + organizationId, + }, + }, + }); +} + +/** + * Create a new employee or reactivate an existing one + */ +export async function createOrReactivateEmployee(params: { + name: string; + email: string; + department: Departments; + organizationId: string; + externalEmployeeId?: string; +}): Promise { + const { name, email, department, organizationId, externalEmployeeId } = + params; + + // First check if an employee exists + const existingEmployee = await findEmployeeByEmail(email, organizationId); + + if (existingEmployee) { + if (existingEmployee.isActive) { + throw new Error( + "An employee with this email already exists in your organization" + ); + } + + // Reactivate the existing employee + return db.employee.update({ + where: { id: existingEmployee.id }, + data: { + name, + department, + isActive: true, + externalEmployeeId, + organizationId, + updatedAt: new Date(), + }, + }); + } + + // Create a new employee + return db.employee.create({ + data: { + name, + email, + department, + organizationId, + isActive: true, + externalEmployeeId, + }, + }); +} + +/** + * Create or update a portal user for an employee + */ +export async function createOrUpdatePortalUser(params: { + employeeId: string; + name: string; + email: string; + organizationId: string; +}): Promise { + const { employeeId, name, email, organizationId } = params; + + await db.portalUser.upsert({ + where: { email }, + create: { + id: employeeId, + name, + email, + organizationId, + emailVerified: false, + createdAt: new Date(), + updatedAt: new Date(), + employees: { + connect: { + id: employeeId, + }, + }, + }, + update: { + updatedAt: new Date(), + name, + email, + organizationId, + employees: { + connect: { + id: employeeId, + }, + }, + }, + }); +} + +/** + * Create default tasks for an employee + */ +export async function createDefaultTasksForEmployee( + employeeId: string +): Promise { + // Create or get the required task definitions first and store their IDs + const requiredTasks = await Promise.all( + DEFAULT_TASKS.map(async (task) => { + return db.employeeRequiredTask.upsert({ + where: { code: task.code }, + create: { + code: task.code, + name: task.name, + description: task.description, + }, + update: {}, + }); + }) + ); + + // Now create the employee tasks using the actual task IDs + await Promise.all( + requiredTasks.map(async (task) => { + return db.employeeTask.create({ + data: { + employeeId: employeeId, + requiredTaskId: task.id, + status: "assigned", + }, + }); + }) + ); +} + +/** + * Complete employee creation by handling all steps: + * 1. Create/reactivate the employee + * 2. Create/update portal user + * 3. Create default tasks + */ +export async function completeEmployeeCreation(params: { + name: string; + email: string; + department: Departments; + organizationId: string; + externalEmployeeId?: string; +}): Promise { + const employee = await createOrReactivateEmployee(params); + + await createOrUpdatePortalUser({ + employeeId: employee.id, + name: params.name, + email: params.email, + organizationId: params.organizationId, + }); + + await createDefaultTasksForEmployee(employee.id); + + return employee; +} diff --git a/apps/app/src/trigger/deel/index.ts b/apps/app/src/trigger/deel/index.ts index 521c9b43f9..d58c6d0122 100644 --- a/apps/app/src/trigger/deel/index.ts +++ b/apps/app/src/trigger/deel/index.ts @@ -4,6 +4,10 @@ import axios from "axios"; import { z } from "zod"; import { Departments } from "@bubba/db"; import { decrypt } from "@/lib/encryption"; +import { + findEmployeeByEmail, + completeEmployeeCreation, +} from "@/lib/db/employee"; const deelTaskSchema = z.object({ integration: z.object({ @@ -187,26 +191,26 @@ export const syncDeelEmployees = schemaTask({ } // Check if we've run this integration within the last 24 hours - // const lastRun = await db.integrationLastRun.findUnique({ - // where: { - // integrationId_organizationId: { - // integrationId: integration.id, - // organizationId: integration.organization.id, - // }, - // }, - // }); + const lastRun = await db.integrationLastRun.findUnique({ + where: { + integrationId_organizationId: { + integrationId: integration.id, + organizationId: integration.organization.id, + }, + }, + }); const now = new Date(); - // if ( - // lastRun && - // lastRun.lastRunAt > new Date(now.getTime() - 24 * 60 * 60 * 1000) - // ) { - // logger.info("Skipping Deel sync as it was run less than 24 hours ago"); - // return { - // success: true, - // message: "Skipped - run less than 24 hours ago", - // }; - // } + if ( + lastRun && + lastRun.lastRunAt > new Date(now.getTime() - 24 * 60 * 60 * 1000) + ) { + logger.info("Skipping Deel sync as it was run less than 24 hours ago"); + return { + success: true, + message: "Skipped - run less than 24 hours ago", + }; + } // Extract access token from user settings let accessToken: string | undefined; @@ -267,8 +271,12 @@ export const syncDeelEmployees = schemaTask({ (emp) => emp.hiring_status === "active" || emp.new_hiring_status === "active" ); + const inactiveEmployees = deelEmployees.filter( + (emp) => + emp.hiring_status !== "active" && emp.new_hiring_status !== "active" + ); logger.info( - `Found ${activeEmployees.length} active employees out of ${deelEmployees.length} total` + `Found ${activeEmployees.length} active employees and ${inactiveEmployees.length} inactive employees out of ${deelEmployees.length} total` ); // Process each employee @@ -276,19 +284,13 @@ export const syncDeelEmployees = schemaTask({ logger.info("Starting to process employees..."); for (const deelEmployee of deelEmployees) { - // Skip inactive employees - check both hiring_status and new_hiring_status - if ( - deelEmployee.hiring_status !== "active" && - deelEmployee.new_hiring_status !== "active" - ) { - logger.info( - `Skipping inactive employee: ${deelEmployee.full_name} (ID: ${deelEmployee.id})` - ); - continue; - } + // Process all employees, both active and inactive + const isActive = + deelEmployee.hiring_status === "active" || + deelEmployee.new_hiring_status === "active"; logger.info( - `Processing employee: ${deelEmployee.full_name} (ID: ${deelEmployee.id})` + `Processing employee: ${deelEmployee.full_name} (ID: ${deelEmployee.id}, Status: ${isActive ? "active" : "inactive"})` ); const name = @@ -333,15 +335,11 @@ export const syncDeelEmployees = schemaTask({ logger.info(`Looking for existing employee with email: ${email}`); - // Check if employee already exists - const existingEmployee = await db.employee.findUnique({ - where: { - email_organizationId: { - email, - organizationId: integration.organization.id, - }, - }, - }); + // Check if employee already exists using the reusable function + const existingEmployee = await findEmployeeByEmail( + email, + integration.organization.id + ); if (existingEmployee) { logger.info( @@ -356,33 +354,58 @@ export const syncDeelEmployees = schemaTask({ name, department, externalEmployeeId: deelEmployee.id, - // Keep isActive true as we're filtering inactive employees already + isActive, // Set isActive based on Deel status }, }); - logger.info(`Successfully updated employee: ${name}`); + logger.info( + `Successfully updated employee: ${name} with isActive: ${isActive}` + ); processedEmployees.push({ id: existingEmployee.id, action: "updated", + isActive, }); } else { logger.info( `No existing employee found for ${email}, creating new employee...` ); - // Create new employee - const newEmployee = await db.employee.create({ - data: { + try { + // Create new employee using the reusable function + const newEmployee = await completeEmployeeCreation({ name, email, department, - isActive: true, - externalEmployeeId: deelEmployee.id, organizationId: integration.organization.id, - }, - }); - logger.info( - `Successfully created new employee: ${name} with ID: ${newEmployee.id}` - ); - processedEmployees.push({ id: newEmployee.id, action: "created" }); + externalEmployeeId: deelEmployee.id, + }); + + // If employee is inactive, update the isActive status after creation + if (!isActive) { + await db.employee.update({ + where: { + id: newEmployee.id, + }, + data: { + isActive: false, + }, + }); + logger.info("Updated new employee to inactive status"); + } + + logger.info( + `Successfully created new employee: ${name} with ID: ${newEmployee.id}, isActive: ${isActive}` + ); + processedEmployees.push({ + id: newEmployee.id, + action: "created", + isActive, + }); + } catch (error) { + logger.error( + `Error creating employee: ${error instanceof Error ? error.message : String(error)}` + ); + // Skip to the next employee if there's an error + } } } From 20335944851d2ee89fe03b47354d97ab9b0a5d3d Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Tue, 11 Mar 2025 14:23:17 -0400 Subject: [PATCH 6/7] feat(employees): add employee status column and enhance table functionality - Introduce employee status column in the People table - Create `EmployeeStatus` component for consistent status display - Add sorting and column header functionality for status column - Update server-side column headers and translations - Refactor data table to support dynamic column rendering - Remove `isActive` from employee creation logic - Improve table layout and responsiveness --- .../components/EmployeeDetails.tsx | 27 +- .../people/actions/get-employees.ts | 6 - .../people/components/EmployeesList.tsx | 103 +++--- apps/app/src/components/status.tsx | 30 +- .../people/data-table-column-header.tsx | 28 ++ .../tables/people/data-table-header.tsx | 186 ++++++----- .../components/tables/people/data-table.tsx | 296 ++++++++++-------- .../tables/people/employee-status.tsx | 39 +++ .../tables/people/server-columns.tsx | 13 +- apps/app/src/lib/db/employee.ts | 2 - apps/app/src/locales/en.ts | 1 + 11 files changed, 428 insertions(+), 303 deletions(-) create mode 100644 apps/app/src/components/tables/people/data-table-column-header.tsx create mode 100644 apps/app/src/components/tables/people/employee-status.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/components/EmployeeDetails.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/components/EmployeeDetails.tsx index ca303647aa..d28520be9b 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/components/EmployeeDetails.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/people/[employeeId]/components/EmployeeDetails.tsx @@ -2,23 +2,14 @@ import { redirect } from "next/navigation"; import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card"; -import { Progress } from "@bubba/ui/progress"; import { useI18n } from "@/locales/client"; -import { cn } from "@bubba/ui/cn"; import { useEmployeeDetails } from "../../hooks/useEmployee"; import { Skeleton } from "@bubba/ui/skeleton"; -import { AlertCircle, CheckCircle2, Info } from "lucide-react"; +import { AlertCircle, CheckCircle2 } from "lucide-react"; import { Alert, AlertDescription, AlertTitle } from "@bubba/ui/alert"; -import type { EmployeeTask } from "../types"; import { Label } from "@bubba/ui/label"; import { formatDate } from "@/utils/format"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@bubba/ui/tabs"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@bubba/ui/accordion"; +import { Button } from "@bubba/ui/button"; interface EmployeeDetailsProps { employeeId: string; } @@ -74,11 +65,7 @@ export function EmployeeDetails({ employeeId }: EmployeeDetailsProps) { const tasks = employee.employeeTasks ?? []; return ( -
-

- {employee.name} ({employee.isActive ? "Active" : "Inactive"}) -

- +
@@ -113,8 +100,11 @@ export function EmployeeDetails({ employeeId }: EmployeeDetailsProps) { const isCompleted = task.status === "completed"; return ( -
-

+
+

{isCompleted ? ( ) : ( @@ -122,6 +112,7 @@ export function EmployeeDetails({ employeeId }: EmployeeDetailsProps) { )} {task.requiredTask.name}

+
); })} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/people/actions/get-employees.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/people/actions/get-employees.ts index b7dab24c24..823417ea8c 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/people/actions/get-employees.ts +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/people/actions/get-employees.ts @@ -42,12 +42,6 @@ export const getEmployees = authActionClient : {}, ], }, - select: { - id: true, - name: true, - email: true, - department: true, - }, skip, take: per_page, }), diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/people/components/EmployeesList.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/people/components/EmployeesList.tsx index cd58bbbb0c..ae6fc93280 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/people/components/EmployeesList.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/people/components/EmployeesList.tsx @@ -2,8 +2,8 @@ import { DataTable } from "@/components/tables/people/data-table"; import { - NoResults, - NoEmployees, + NoResults, + NoEmployees, } from "@/components/tables/people/empty-states"; import { FilterToolbar } from "@/components/tables/people/filter-toolbar"; import { Loading } from "@/components/tables/people/loading"; @@ -13,60 +13,65 @@ import type { PersonType } from "@/components/tables/people/columns"; import { EmployeesListSkeleton } from "./EmployeesListSkeleton"; interface EmployeesListProps { - columnHeaders: { - name: string; - email: string; - department: string; - }; + columnHeaders: { + name: string; + email: string; + department: string; + status: string; + }; } export function EmployeesList({ columnHeaders }: EmployeesListProps) { - const searchParams = useSearchParams(); - const search = searchParams.get("search"); - const role = searchParams.get("role"); - const per_page = Number(searchParams.get("per_page")) || 10; - const page = Number(searchParams.get("page")) || 1; + const searchParams = useSearchParams(); + const search = searchParams.get("search"); + const role = searchParams.get("role"); + const per_page = Number(searchParams.get("per_page")) || 10; + const page = Number(searchParams.get("page")) || 1; - const { employees, total, isLoading, error } = useEmployees(); + const { employees, total, isLoading, error } = useEmployees(); - if (isLoading) { - return ; - } + console.log({ + employees, + }); - if (error) { - return ( -
- - -
- ); - } + if (isLoading) { + return ; + } - const hasFilters = !!(search || role); + if (error) { + return ( +
+ + +
+ ); + } - if (employees.length === 0 && !hasFilters) { - return ( -
- - - -
- ); - } + const hasFilters = !!(search || role); - return ( -
- - {employees.length > 0 ? ( - - ) : ( - - )} -
- ); + if (employees.length === 0 && !hasFilters) { + return ( +
+ + + +
+ ); + } + + return ( +
+ + {employees.length > 0 ? ( + + ) : ( + + )} +
+ ); } diff --git a/apps/app/src/components/status.tsx b/apps/app/src/components/status.tsx index f0c945ae98..0188d658a0 100644 --- a/apps/app/src/components/status.tsx +++ b/apps/app/src/components/status.tsx @@ -4,26 +4,26 @@ import { cn } from "@bubba/ui/cn"; export const STATUS_TYPES = ["open", "pending", "closed"] as const; export type StatusType = Exclude< - (typeof STATUS_TYPES)[number], - "draft" | "published" + (typeof STATUS_TYPES)[number], + "draft" | "published" >; const STATUS_COLORS: Record = { - open: "#ffc107", - pending: "#0ea5e9", - closed: "#00DC73", + open: "#ffc107", + pending: "#0ea5e9", + closed: "#00DC73", } as const; export function Status({ status }: { status: StatusType }) { - const t = useI18n(); + const t = useI18n(); - return ( -
-
- {t(`common.status.${status}`)} -
- ); + return ( +
+
+ {t(`common.status.${status}`)} +
+ ); } diff --git a/apps/app/src/components/tables/people/data-table-column-header.tsx b/apps/app/src/components/tables/people/data-table-column-header.tsx new file mode 100644 index 0000000000..27241376e1 --- /dev/null +++ b/apps/app/src/components/tables/people/data-table-column-header.tsx @@ -0,0 +1,28 @@ +"use client"; + +import type { Column } from "@tanstack/react-table"; +import { Button } from "@bubba/ui/button"; +import { TableHead } from "@bubba/ui/table"; + +interface DataTableColumnHeaderProps { + column: Column; + title: string; + className?: string; +} + +export function DataTableColumnHeader({ + column, + title, + className, +}: DataTableColumnHeaderProps) { + return ( + + + + ); +} diff --git a/apps/app/src/components/tables/people/data-table-header.tsx b/apps/app/src/components/tables/people/data-table-header.tsx index f97e8cb5dd..d6d8b11622 100644 --- a/apps/app/src/components/tables/people/data-table-header.tsx +++ b/apps/app/src/components/tables/people/data-table-header.tsx @@ -9,99 +9,115 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { useCallback } from "react"; type Props = { - table?: { - getIsAllPageRowsSelected: () => boolean; - getIsSomePageRowsSelected: () => boolean; - getAllLeafColumns: () => { - id: string; - getIsVisible: () => boolean; - }[]; - toggleAllPageRowsSelected: (value: boolean) => void; - }; - loading?: boolean; - isEmpty?: boolean; + table?: { + getIsAllPageRowsSelected: () => boolean; + getIsSomePageRowsSelected: () => boolean; + getAllLeafColumns: () => { + id: string; + getIsVisible: () => boolean; + }[]; + toggleAllPageRowsSelected: (value: boolean) => void; + }; + loading?: boolean; + isEmpty?: boolean; }; export function DataTableHeader({ table, loading }: Props) { - const searchParams = useSearchParams(); - const pathname = usePathname(); - const router = useRouter(); - const t = useI18n(); + const searchParams = useSearchParams(); + const pathname = usePathname(); + const router = useRouter(); + const t = useI18n(); - const sortParam = searchParams.get("sort"); - const [column, value] = sortParam ? sortParam.split(":") : []; + const sortParam = searchParams.get("sort"); + const [column, value] = sortParam ? sortParam.split(":") : []; - const createSortQuery = useCallback( - (name: string) => { - const params = new URLSearchParams(searchParams); - const prevSort = params.get("sort"); + const createSortQuery = useCallback( + (name: string) => { + const params = new URLSearchParams(searchParams); + const prevSort = params.get("sort"); - if (`${name}:asc` === prevSort) { - params.set("sort", `${name}:desc`); - } else if (`${name}:desc` === prevSort) { - params.delete("sort"); - } else { - params.set("sort", `${name}:asc`); - } + if (`${name}:asc` === prevSort) { + params.set("sort", `${name}:desc`); + } else if (`${name}:desc` === prevSort) { + params.delete("sort"); + } else { + params.set("sort", `${name}:asc`); + } - router.replace(`${pathname}?${params.toString()}`); - }, - [searchParams, router, pathname], - ); + router.replace(`${pathname}?${params.toString()}`); + }, + [searchParams, router, pathname], + ); - const isVisible = (id: string) => - loading || - table - ?.getAllLeafColumns() - .find((col) => col.id === id) - ?.getIsVisible(); + const isVisible = (id: string) => + loading || + table + ?.getAllLeafColumns() + .find((col) => col.id === id) + ?.getIsVisible(); - return ( - - - {isVisible("email") && ( - - - - )} - {isVisible("name") && ( - - - - )} + return ( + + + {isVisible("email") && ( + + + + )} + {isVisible("name") && ( + + + + )} - {isVisible("department") && ( - - - - )} - - - ); + {isVisible("department") && ( + + + + )} + + {isVisible("status") && ( + + + + )} + + + ); } diff --git a/apps/app/src/components/tables/people/data-table.tsx b/apps/app/src/components/tables/people/data-table.tsx index 548d96606c..fa43b26e68 100644 --- a/apps/app/src/components/tables/people/data-table.tsx +++ b/apps/app/src/components/tables/people/data-table.tsx @@ -1,150 +1,202 @@ "use client"; import { - type ColumnDef, - flexRender, - getCoreRowModel, - useReactTable, + type ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, } from "@tanstack/react-table"; import { useI18n } from "@/locales/client"; import { Button } from "@bubba/ui/button"; import { cn } from "@bubba/ui/cn"; import { - Table, - TableBody, - TableCell, - TableHead, - TableRow, + Table, + TableBody, + TableCell, + TableHead, + TableRow, } from "@bubba/ui/table"; import { useRouter } from "next/navigation"; import type { PersonType } from "./columns"; import { DataTableHeader } from "./data-table-header"; import { DataTablePagination } from "./data-table-pagination"; +import { DataTableColumnHeader } from "./data-table-column-header"; +import { Badge } from "@bubba/ui/badge"; +import { Status, type StatusType } from "@/components/status"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@bubba/ui/dropdown-menu"; +import { + EmployeeStatus, + getEmployeeStatusFromBoolean, +} from "./employee-status"; interface DataTableProps { - columnHeaders: { - name: string; - email: string; - department: string; - }; - data: PersonType[]; - pageCount: number; - currentPage: number; + columnHeaders: { + name: string; + email: string; + department: string; + status: string; + }; + data: PersonType[]; + pageCount: number; + currentPage: number; } function getColumns(): ColumnDef[] { - const t = useI18n(); + const t = useI18n(); + + return [ + { + id: "email", + accessorKey: "email", + header: ({ column }) => ( + + + + ), + }, + { + id: "name", + accessorKey: "name", + header: ({ column }) => ( + + + + ), + }, + { + id: "department", + accessorKey: "department", + header: ({ column }) => ( + + + + ), + cell: ({ row }) => { + const department = row.original.department; + return ( +
+ {department} +
+ ); + }, + }, + { + id: "status", + accessorKey: "isActive", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const isActive = row.original.isActive; + const status = getEmployeeStatusFromBoolean(isActive); - return [ - { - id: "email", - accessorKey: "email", - header: ({ column }) => ( - - - - ), - }, - { - id: "name", - accessorKey: "name", - header: ({ column }) => ( - - - - ), - }, - { - id: "department", - accessorKey: "department", - header: ({ column }) => ( - - - - ), - }, - ]; + return ; + }, + enableSorting: true, + enableHiding: true, + }, + ]; } export function DataTable({ - columnHeaders, - data, - pageCount, - currentPage, + columnHeaders, + data, + pageCount, + currentPage, }: DataTableProps) { - const router = useRouter(); - const clientColumns = getColumns(); - const columns = clientColumns.map((col) => ({ - ...col, - header: columnHeaders[col.id as keyof typeof columnHeaders], - accessorFn: (row: PersonType) => row[col.id as keyof PersonType], - })); + const router = useRouter(); + const clientColumns = getColumns(); + const columns = clientColumns.map((col) => ({ + ...col, + header: columnHeaders[col.id as keyof typeof columnHeaders], + accessorFn: (row: PersonType) => row[col.id as keyof PersonType], + })); + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + pageCount, + }); + + return ( +
+ + - const table = useReactTable({ - data, - columns, - getCoreRowModel: getCoreRowModel(), - manualPagination: true, - pageCount, - }); + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + { + const person = row.original; + router.push(`/people/${person.id}`); + }} + > + {row.getVisibleCells().map((cell) => { + let cellClassName = ""; - return ( -
-
- + if (cell.column.id === "name") { + cellClassName = "w-[30%]"; + } else if (cell.column.id === "email") { + cellClassName = "w-[30%] hidden md:table-cell"; + } else if (cell.column.id === "department") { + cellClassName = "w-[20%]"; + } else if (cell.column.id === "status") { + cellClassName = "w-[20%] text-center"; + } - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - { - const person = row.original; - router.push(`/people/${person.id}`); - }} - > - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )) - ) : ( - - - No results. - - - )} - -
- -
- ); + return ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ); + })} + + )) + ) : ( + + + No results. + + + )} + + + +
+ ); } diff --git a/apps/app/src/components/tables/people/employee-status.tsx b/apps/app/src/components/tables/people/employee-status.tsx new file mode 100644 index 0000000000..21095ee726 --- /dev/null +++ b/apps/app/src/components/tables/people/employee-status.tsx @@ -0,0 +1,39 @@ +import { useI18n } from "@/locales/client"; +import { cn } from "@bubba/ui/cn"; + +// Define employee status types +export const EMPLOYEE_STATUS_TYPES = ["active", "inactive"] as const; +export type EmployeeStatusType = (typeof EMPLOYEE_STATUS_TYPES)[number]; + +// Define colors for employee statuses +const EMPLOYEE_STATUS_COLORS: Record = { + active: "#00DC73", // Green - Same as "closed" in Status component + inactive: "#ffc107", // Yellow/amber - Same as "open" in Status component +}; + +/** + * EmployeeStatus component that matches the styling of the Status component + * but uses active/inactive states specific to employees + */ +export function EmployeeStatus({ status }: { status: EmployeeStatusType }) { + const t = useI18n(); + + return ( +
+
+ {t(`common.status.${status}`)} +
+ ); +} + +/** + * Converts boolean isActive to EmployeeStatusType + */ +export function getEmployeeStatusFromBoolean( + isActive: boolean, +): EmployeeStatusType { + return isActive ? "active" : "inactive"; +} diff --git a/apps/app/src/components/tables/people/server-columns.tsx b/apps/app/src/components/tables/people/server-columns.tsx index e0b0ff7c18..794e0e972a 100644 --- a/apps/app/src/components/tables/people/server-columns.tsx +++ b/apps/app/src/components/tables/people/server-columns.tsx @@ -1,11 +1,12 @@ import { getI18n } from "@/locales/server"; export async function getServerColumnHeaders() { - const t = await getI18n(); + const t = await getI18n(); - return { - name: t("people.table.name"), - email: t("people.table.email"), - department: t("people.table.department"), - }; + return { + name: t("people.table.name"), + email: t("people.table.email"), + department: t("people.table.department"), + status: t("people.table.status"), + }; } diff --git a/apps/app/src/lib/db/employee.ts b/apps/app/src/lib/db/employee.ts index 9a3a486698..afade935d5 100644 --- a/apps/app/src/lib/db/employee.ts +++ b/apps/app/src/lib/db/employee.ts @@ -60,7 +60,6 @@ export async function createOrReactivateEmployee(params: { data: { name, department, - isActive: true, externalEmployeeId, organizationId, updatedAt: new Date(), @@ -75,7 +74,6 @@ export async function createOrReactivateEmployee(params: { email, department, organizationId, - isActive: true, externalEmployeeId, }, }); diff --git a/apps/app/src/locales/en.ts b/apps/app/src/locales/en.ts index 8587681a3a..92e0756d80 100644 --- a/apps/app/src/locales/en.ts +++ b/apps/app/src/locales/en.ts @@ -608,6 +608,7 @@ export default { name: "Employee Name", email: "Email", department: "Department", + status: "Status", externalId: "External ID", }, empty: { From b7860aa06af406d63bc875c73e80467ade9d94a1 Mon Sep 17 00:00:00 2001 From: Languine Bot Date: Tue, 11 Mar 2025 18:24:05 +0000 Subject: [PATCH 7/7] chore: (i18n) update translations using Languine.ai --- apps/app/languine.lock | 1 + apps/app/src/locales/es.ts | 3 ++- apps/app/src/locales/fr.ts | 3 ++- apps/app/src/locales/no.ts | 3 ++- apps/app/src/locales/pt.ts | 3 ++- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/app/languine.lock b/apps/app/languine.lock index a7739424fd..fb87203294 100644 --- a/apps/app/languine.lock +++ b/apps/app/languine.lock @@ -405,6 +405,7 @@ files: people.table.name: 717231b5b2cf0543d46d6555f25c56b4 people.table.email: ce8ae9da5b7cd6c3df2929543a9af92d people.table.department: 1d17cb9923b99f823da9f5a16dc460e5 + people.table.status: ec53a8c4f07baed5d8825072c89799be people.table.externalId: 01ddb49044c161b11fdb0c41adbeb3b3 people.empty.no_employees.title: 9047cc33a16248133c3fcc8884523588 people.empty.no_employees.description: 3fdb36a3d56c06043da562aaa1045825 diff --git a/apps/app/src/locales/es.ts b/apps/app/src/locales/es.ts index fae9540058..287929af28 100644 --- a/apps/app/src/locales/es.ts +++ b/apps/app/src/locales/es.ts @@ -766,7 +766,8 @@ export default { name: "Nombre del empleado", email: "Correo Electrónico", department: "Departamento", - externalId: "ID Externo" + externalId: "ID Externo", + status: "Estado" }, empty: { no_employees: { diff --git a/apps/app/src/locales/fr.ts b/apps/app/src/locales/fr.ts index 94582af728..2df71afe91 100644 --- a/apps/app/src/locales/fr.ts +++ b/apps/app/src/locales/fr.ts @@ -766,7 +766,8 @@ export default { name: "Nom de l'employé", email: "Email", department: "Département", - externalId: "ID externe" + externalId: "ID externe", + status: "Statut" }, empty: { no_employees: { diff --git a/apps/app/src/locales/no.ts b/apps/app/src/locales/no.ts index a1bb43fa9e..1adc667e3d 100644 --- a/apps/app/src/locales/no.ts +++ b/apps/app/src/locales/no.ts @@ -766,7 +766,8 @@ export default { name: "Ansattnavn", email: "E-post", department: "Avdeling", - externalId: "Ekstern ID" + externalId: "Ekstern ID", + status: "Status" }, empty: { no_employees: { diff --git a/apps/app/src/locales/pt.ts b/apps/app/src/locales/pt.ts index 7c1dce53c6..c973ba858f 100644 --- a/apps/app/src/locales/pt.ts +++ b/apps/app/src/locales/pt.ts @@ -766,7 +766,8 @@ export default { name: "Nome do Funcionário", email: "Email", department: "Departamento", - externalId: "ID Externo" + externalId: "ID Externo", + status: "Status" }, empty: { no_employees: {