238 Commits

Author SHA1 Message Date
Julien Froidefond
af41531597 feat(JiraSync): enhance synchronization logic to preserve original Jira actions and detect changes
- Updated the Jira synchronization process to include original Jira actions for better detail retention.
- Implemented a new function to detect real changes and preserved fields during task synchronization.
- Enhanced the UI to display actions with preserved fields separately for improved clarity.
- Added comprehensive tests for the new change detection logic to ensure accuracy and reliability.
2025-11-21 14:14:30 +01:00
Julien Froidefond
d9e7a05f14 chore(package): update version to 1.0.0 for release 2025-11-21 11:12:40 +01:00
Julien Froidefond
4e4c347250 chore(package): add auto-version script 2025-11-21 11:12:11 +01:00
Julien Froidefond
8bdd3a8253 feat(tests): integrate Vitest for testing framework and add test scripts
- Added Vitest as a dependency for improved testing capabilities.
- Updated package.json with new test scripts for running tests, watching, and coverage reporting.
- Configured ESLint to recognize test runner scripts and included them in the linting process.
- Modified tsconfig.json to include Vitest types for better TypeScript support in tests.
2025-11-21 10:40:30 +01:00
Julien Froidefond
31f9855a3c feat(TaskManagement): implement centralized readonly field logic for task synchronization
- Added functionality to determine readonly fields based on task source (Jira, TFS) and status.
- Updated EditTaskForm and TaskBasicFields components to utilize readonly fields for better user experience.
- Introduced buildSyncUpdateData function to manage field preservation during synchronization.
- Enhanced tests for readonly field logic to ensure correct behavior across different scenarios.
2025-11-21 10:40:21 +01:00
Julien Froidefond
b8256a18b6 fix(icons): correct icon source reference in script for consistency 2025-11-18 09:28:46 +01:00
Julien Froidefond
f404f06d14 chore(icons): update icon assets and script source image reference
- Replaced existing icon assets with updated versions for better visual quality.
- Modified the script to generate icons from the new source image 'iconTC4S.png' instead of 'iconTC2.png'.
2025-11-18 08:39:05 +01:00
Julien Froidefond
deb3097047 feat(package): add sharp library for image processing and update dependencies
- Added sharp version 0.34.5 to package.json for enhanced image processing capabilities.
- Updated pnpm-lock.yaml to reflect the new sharp version and its dependencies.
- Included additional icons in layout metadata for improved application branding.
2025-11-17 11:36:59 +01:00
Julien Froidefond
a9a2988293 feat(MarkdownEditor, NotesList): add editing change handler and improve delete confirmation text encoding 2025-11-17 08:30:02 +01:00
Julien Froidefond
72cd76c77b style(NotesList): enhance delete confirmation UI with improved styling and dynamic note title display 2025-11-17 08:29:26 +01:00
Julien Froidefond
6cad6a333d feat(NotesPage, MarkdownEditor): enhance note creation and editing experience
- Added state management for new notes in NotesPageClient to track if a note is newly created.
- Updated MarkdownEditor to support initial editing state and handle editing changes, improving user interaction during note creation and editing.
2025-11-17 08:27:34 +01:00
Julien Froidefond
f0b9f75817 fix(DailyCheckboxItem): adjust emoji display size for better responsiveness 2025-11-12 09:38:45 +01:00
Julien Froidefond
8340008839 feat(DailyPage, DailyService, Calendar): enhance task deadline management and UI integration
- Implemented user authentication in the daily dates API route to ensure secure access.
- Added functionality to retrieve task deadlines and associated tasks, improving task management capabilities.
- Updated DailyPageClient to display tasks with deadlines in the calendar view, enhancing user experience.
- Enhanced Calendar component to visually indicate deadline dates, providing clearer task management context.
2025-11-11 08:46:19 +01:00
Julien Froidefond
f7c9926348 feat(auth): enhance authentication process with secure cookie handling and detailed logging
- Implemented secure cookie options based on HTTPS detection to improve security.
- Added detailed logging for credential checks and user authentication flow to aid in debugging and monitoring.
2025-11-10 23:15:53 +01:00
Julien Froidefond
c7c47039b4 feat(EditCheckboxModal, ObjectivesBoard, StatusBadge): enhance task filtering and status handling
- Improved task filtering in EditCheckboxModal to prioritize non-completed tasks and enhance relevance scoring.
- Updated ObjectivesBoard to support dynamic visibility of task statuses and improved layout for better user experience.
- Enhanced StatusBadge component to support size variations and customizable display options for task statuses.
- Added new CSS variables for task priority colors in globals.css to standardize priority indicators across the application.
2025-11-10 09:09:28 +01:00
Julien Froidefond
2d4c161e1d chore(README): enhance project structure documentation and clarify folder purposes
- Updated README.md to provide a detailed explanation of the project structure, including descriptions for each directory and its contents.
- Improved clarity on the organization of Next.js pages, API routes, components, services, and utilities.
2025-11-05 08:05:13 +01:00
Julien Froidefond
9fc355abad feat(DailyCheckboxItem, TaskCard, DailyService): enhance task emoji handling and improve data fetching
- Added emoji support in DailyCheckboxItem and TaskCard components using getTaskEmoji.
- Updated DailyService to include taskTags and primaryTag in checkbox data fetching, improving task detail retrieval.
- Refactored mapPrismaCheckbox to handle taskTags and primaryTag extraction for better task representation.
2025-11-03 09:29:37 +01:00
Julien Froidefond
08f3fb6e85 chore(docker): update database path and permissions in Docker configuration
- Modified docker-compose.yml to change DATABASE_URL path for consistency.
- Updated Dockerfile to copy Prisma schema and set a temporary DATABASE_URL for client generation.
- Enhanced CMD to ensure proper permissions for the data directory and user switching during application startup.
- Changed README.md file permissions to executable.
2025-10-31 14:00:50 +01:00
Julien Froidefond
e4e49df60b chore: update configuration and improve backup service handling
- Added root path configuration for turbopack in next.config.ts.
- Updated build script in package.json to include Prisma generation.
- Changed backup service methods to use synchronous config retrieval where appropriate, improving performance and avoiding async issues.
- Ensured dynamic rendering in layout.tsx for better page performance.
2025-10-31 12:11:19 +01:00
Julien Froidefond
5d1239c4de chore(docker): refactor Docker configuration for environment variables and database initialization
- Updated docker-compose.yml to use environment variable fallbacks for configuration.
- Modified Dockerfile to streamline database initialization using Prisma migrations directly.
- Removed init-db.js script as its functionality is now integrated into the Docker CMD.
2025-10-31 12:00:01 +01:00
Julien Froidefond
48e649cf75 chore(docker): change exposed port from 3007 to 3006 in docker-compose.yml 2025-10-30 11:23:22 +01:00
Julien Froidefond
76394375ea chore(docker): update Docker configuration for database initialization
- Changed exposed port from 3006 to 3007 in docker-compose.yml.
- Updated Dockerfile to copy init-db.js script and modified CMD to use it for database initialization instead of Prisma migrations.
2025-10-30 11:22:40 +01:00
Julien Froidefond
0bf9802e71 feat(DailyService): implement toggleCheckbox method for direct checkbox state updates 2025-10-30 08:15:01 +01:00
Julien Froidefond
cd391506ce fix(JiraService): add support for cancelled and abandoned statuses in Jira integration 2025-10-27 09:04:55 +01:00
Julien Froidefond
3e19121cb2 fix(FormField): ensure full-width styling for input fields 2025-10-27 08:37:24 +01:00
Julien Froidefond
fd46ed180f fix(globals.css): improve select appearance in Safari 2025-10-27 08:18:23 +01:00
Julien Froidefond
f7f77a49dc feat(MarkdownEditor): enhance Markdown rendering with new plugins and components
- Integrated rehype-raw and rehype-slug for improved Markdown processing.
- Added remark-toc for automatic table of contents generation.
- Refactored Markdown components for better styling and functionality.
- Updated package.json to include new dependencies for enhanced Markdown features.
2025-10-24 09:49:56 +02:00
Julien Froidefond
b60e74b1ff feat(DailyPageClient): add pending refresh trigger for daily dates updates
- Introduced a new state variable `pendingRefreshTrigger` to manage refresh actions for daily dates.
- Updated relevant functions to increment the trigger upon checkbox actions and date refreshes, ensuring UI updates reflect the latest data.
2025-10-23 13:02:13 +02:00
Julien Froidefond
87acb3709d chore: migrate from npm to pnpm for package management across documentation and scripts 2025-10-16 06:16:37 +02:00
Julien Froidefond
2b9205007f chore(TasksContext): remove debug logging for old completed tasks 2025-10-16 06:00:28 +02:00
Julien Froidefond
3b7a6c3972 fix(JiraService): preserve archived status when updating tasks 2025-10-16 05:54:52 +02:00
Julien Froidefond
a4188b09e5 fix: suppress hydration warnings in JiraLogs, MarkdownEditor, and NotesList components 2025-10-16 05:17:41 +02:00
Julien Froidefond
7952459b42 feat(Tags): implement user-specific tag management and enhance related services
- Added ownerId field to Tag model to associate tags with users.
- Updated tagsService methods to enforce user ownership in tag operations.
- Enhanced API routes to include user authentication and ownership checks for tag retrieval and management.
- Modified seeding script to assign tags to the first user found in the database.
- Updated various components and services to ensure user-specific tag handling throughout the application.
2025-10-11 15:03:59 +02:00
Julien Froidefond
583efaa8c5 feat(KanbanFilters): add filter for hiding completed tasks older than 7 days
- Enhanced KanbanFilters component to include a new filter option for hiding tasks completed more than 15 days ago.
- Updated GeneralFilters component to display the new filter and its count.
- Modified TasksContext to calculate and provide the count of old completed tasks.
- Adjusted KanbanFiltersProps and related types to accommodate the new filter functionality.
2025-10-10 17:02:20 +02:00
Julien Froidefond
5dcfa19b0c feat(HeaderNavigation): update navigation to use user preferences for Jira configuration
- Replaced Jira configuration context with user preferences context to determine Jira setup.
- Enhanced navigation links to reflect user-specific Jira project key.
- Fixed CSS class syntax for hover effects and adjusted link display logic for larger screens.
2025-10-10 15:48:01 +02:00
Julien Froidefond
67515441fb fix: duplication on markdownRenderer 2025-10-10 13:48:16 +02:00
Julien Froidefond
75f27c69ee feat(MarkdownEditor): integrate Mermaid support for diagram rendering in Markdown
- Added MermaidRenderer component to handle Mermaid diagrams within Markdown content.
- Enhanced preformatted code block handling to detect and render Mermaid syntax.
- Updated package.json and package-lock.json to include Mermaid dependency for diagram support.
2025-10-10 12:00:56 +02:00
Julien Froidefond
8cb0dcf3af feat(Task): implement user ownership for tasks and enhance related services
- Added ownerId field to Task model to associate tasks with users.
- Updated TaskService methods to enforce user ownership in task operations.
- Enhanced API routes to include user authentication and ownership checks.
- Modified DailyService and analytics services to filter tasks by user.
- Integrated user session handling in various components for personalized task management.
2025-10-10 11:36:10 +02:00
Julien Froidefond
6bfcd1f100 feat(DailyCheckbox): associate checkboxes with users and enhance daily view functionality
- Added userId field to DailyCheckbox model for user association.
- Updated DailyService methods to handle user-specific checkbox retrieval and management.
- Integrated user authentication checks in API routes and actions for secure access to daily data.
- Enhanced DailyPage to display user-specific daily views, ensuring proper session handling.
- Updated client and service interfaces to reflect changes in data structure.
2025-10-10 08:54:52 +02:00
Julien Froidefond
6748799a90 fix(IconsSection, ToastSection): correct HTML entity usage in French text
- Replaced apostrophes with HTML entities in French phrases for proper rendering.
- Ensured consistency in text presentation across UI components.
2025-10-10 08:25:38 +02:00
Julien Froidefond
e7cbd56e89 feat(DateTimeInput): add calendar picker functionality to DateTimeInput component
- Introduced a ref to manage the input element for triggering the native calendar picker.
- Enhanced the Calendar icon with a click handler to open the date picker, improving user interaction.
- Updated styles for the Calendar icon to include hover effects for better visual feedback.
2025-10-10 08:24:33 +02:00
Julien Froidefond
52d8332f0c refactor(TaskSelector): enhance task selection logic and integrate shared component
- Replaced TaskSelector with TaskSelectorWithData to streamline task selection.
- Updated TaskSelector to accept tasks as a prop, improving data handling.
- Removed unnecessary API calls and loading states, simplifying the component's logic.
- Added new sections to UIShowcaseClient for better component visibility.
2025-10-10 08:22:44 +02:00
Julien Froidefond
7811453e02 feat(Notes): associate notes with tasks and enhance note management
- Added taskId field to Note model for associating notes with tasks.
- Updated API routes to handle taskId in note creation and updates.
- Enhanced NotesPageClient to manage task associations within notes.
- Integrated task selection in MarkdownEditor for better user experience.
- Updated NotesService to map task data correctly when retrieving notes.
2025-10-10 08:05:32 +02:00
Julien Froidefond
ab4a7b3b3e feat: integrate EmojiPickerProvider and add emoji selector shortcut
- Wrapped the layout with EmojiPickerProvider to enable emoji selection functionality.
- Added a new keyboard shortcut (Ctrl/Cmd + Space) for opening the emoji selector, enhancing user experience.
2025-10-09 22:05:20 +02:00
Julien Froidefond
0b17934ca1 refactor(TagInput): optimize dropdown position handling and improve tag loading logic
- Replaced the dropdown position update logic with a dedicated calculatePosition function for clarity.
- Introduced a new state to track if popular tags have been loaded, enhancing the suggestion display logic.
- Cleaned up unnecessary event listeners and streamlined the component's focus handling.
2025-10-09 21:47:59 +02:00
Julien Froidefond
7d4ab33fca feat(Notes): review style of action part of Notes 2025-10-09 16:33:56 +02:00
Julien Froidefond
1c28d6b782 feat: polish notes glass ui 2025-10-09 16:23:10 +02:00
Julien Froidefond
d6538356a1 fix: neutralize bold weight in markdown preview 2025-10-09 14:49:16 +02:00
Julien Froidefond
65e1a3c2d0 docs: document repo guidelines and markdown styling 2025-10-09 14:48:37 +02:00
Julien Froidefond
ae22535dd0 refactor: improve type safety in CustomLabel for StatusDistributionChart component
- Updated the CustomLabel component to use PieLabelRenderProps for better type definitions.
- Added type guards to ensure numeric values are validated before rendering labels, enhancing robustness.
2025-10-09 14:01:36 +02:00
Julien Froidefond
0ffcec7ffc refactor: update CustomTooltip types in chart components for better type safety
- Enhanced type definitions for the payload in CustomTooltip across multiple chart components to improve TypeScript support and maintainability.
2025-10-09 13:50:10 +02:00
Julien Froidefond
d9cf9a2655 chore: prettier everywhere 2025-10-09 13:40:03 +02:00
Julien Froidefond
f8100ae3e9 chore: update pre-commit hook and clean up avatar and gravatar files by removing extra blank lines 2025-10-09 13:38:30 +02:00
Julien Froidefond
6c86ce44f1 feat: add notes feature and keyboard shortcuts
- Introduced a new Note model in the Prisma schema to support note-taking functionality.
- Updated the HeaderNavigation component to include a link to the new Notes page.
- Implemented keyboard shortcuts for note actions, enhancing user experience and productivity.
- Added dependencies for markdown rendering and formatting tools to support note content.
2025-10-09 13:38:09 +02:00
Julien Froidefond
1fe59f26e4 chore: clean up avatar and gravatar files by removing extra blank lines for improved readability 2025-10-09 13:26:57 +02:00
Julien Froidefond
17dade54e6 Remove prettier test file 2025-10-09 11:46:41 +02:00
Julien Froidefond
f98247c142 Test prettier formatting 2025-10-09 11:46:25 +02:00
Julien Froidefond
1499394438 fix: docker KO, emoji empty and adding some todos in doc 2025-10-08 08:32:43 +02:00
Julien Froidefond
8bb5495e13 fix: remove size prop from Emoji component for consistency
- Eliminated the size prop from the Emoji component across various files to standardize rendering and improve code cleanliness.
2025-10-06 09:09:17 +02:00
Julien Froidefond
cd35d67306 fix: remove unused import in StatCard component for cleaner code 2025-10-06 08:17:35 +02:00
Julien Froidefond
714f8ccd5e feat: integrate emoji-mart and refactor emoji usage
- Added @emoji-mart/data and @emoji-mart/react dependencies for enhanced emoji support.
- Replaced static emoji characters with Emoji component in various UI components for consistency and improved rendering.
- Updated generateDateTitle function to return an object with emoji and text for better structure.
- Marked the task for removing emojis from the UI as complete in TODO.md.
2025-10-05 20:29:46 +02:00
Julien Froidefond
7490c38d55 feat: update profile and dashboard components
- Simplified avatar rendering logic in ProfilePage by removing unnecessary eslint-disable comments.
- Added primaryTagId and availableTags props to RecentTasks for better task management.
- Cleaned up imports in TagDistributionChart by removing unused PieLabelRenderProps.
- Removed redundant mobile link class function in HeaderMobile for improved readability.
2025-10-04 11:52:33 +02:00
Julien Froidefond
b2a8c961a8 feat: enhance Jira sync and update TODO.md
- Added handling for unknown statuses in Jira sync, logging them for better debugging and mapping to "todo" by default.
- Updated sync result structure to include unknown statuses and reflected this in the UI for visibility.
- Adjusted JQL to include recently resolved tasks for better status updates during sync.
- Marked the integration of unknown status handling as complete in TODO.md.
2025-10-04 11:49:41 +02:00
Julien Froidefond
ffd3eb998a feat: enhance avatar handling and update TODO.md
- Added Avatar component with support for custom URLs and Gravatar integration, improving user profile visuals.
- Implemented logic to determine avatar source based on user preferences in profile actions.
- Updated ProfilePage to utilize the new Avatar component for better consistency.
- Marked the integration of Gravatar and custom avatar handling as complete in TODO.md.
2025-10-04 11:35:08 +02:00
Julien Froidefond
ad0b723e00 feat: update TODO.md and refactor Header component
- Removed redundant theme handling code from Header component, improving readability and maintainability.
- Integrated HeaderMobile and HeaderDesktop components for better responsive design.
- Marked the task for repositioning the theme icon in the header as complete in TODO.md.
2025-10-04 11:06:49 +02:00
Julien Froidefond
89af1fc597 feat: refactor theme handling and update TODO.md
- Replaced references from theme-config to ui-config for better organization and clarity in theme management.
- Updated Solarized icon in ui-config to a pill emoji for improved visual representation.
- Marked the Solarized icon correction task as complete in TODO.md.
- Deleted the now redundant theme-config file to streamline the codebase.
2025-10-04 10:53:57 +02:00
Julien Froidefond
052b2c2c66 feat: conditionally render refresh button in ManagerWeeklySummary component
- Wrapped the refresh button in a conditional check to only display when the active view is not 'metrics', improving UI clarity and preventing unnecessary actions in the metrics view.
2025-10-04 10:48:20 +02:00
Julien Froidefond
34f1a62435 feat: replace Input with DateTimeInput component in forms and modals
- Updated CreateTaskForm, TaskBasicFields, and EditCheckboxModal to use DateTimeInput for date selection, enhancing consistency and user experience.
- Improved UI by integrating lucide-react Calendar icon in DateTimeInput for better visual feedback.
- Marked EditModal task color issue as complete in TODO.md.
2025-10-04 10:47:27 +02:00
Julien Froidefond
35bda37599 feat: enhance Calendar component legend styling
- Updated the legend in the Calendar component for improved visual clarity.
- Adjusted spacing, added borders, and modified item sizes for better alignment and readability.
- Ensured consistent text styling for legend items.
2025-10-04 10:41:31 +02:00
Julien Froidefond
94145c1ffd feat: integrate lucide-react icons in DailyAddForm and DailySection
- Replaced text icons with lucide-react icons for 'task' and 'meeting' options in DailyAddForm and DailySection for improved visual consistency.
- Updated DailyAddForm to use ToggleButton for better UI interaction and added default icons for options.
- Enhanced FormsSection to reflect these changes in the DailyAddForm usage.
2025-10-04 10:38:37 +02:00
Julien Froidefond
eac9e9a0bb feat: update TODO.md and enhance dashboard components
- Marked several UI/UX tasks as complete in TODO.md, including improvements for Kanban icons, tag visibility, recent tasks display, and header responsiveness.
- Updated PriorityDistributionChart to adjust height for better layout.
- Refined IntegrationFilter to improve filter display and added new trigger class for dropdowns.
- Replaced RecentTaskTimeline with TaskCard in RecentTasks for better consistency.
- Enhanced TagDistributionChart with improved tooltip and legend styling.
- Updated DesktopControls and MobileControls to use lucide-react icons for filters and search functionality.
- Removed RecentTaskTimeline component for cleaner codebase.
2025-10-04 07:17:39 +02:00
Julien Froidefond
c7ad1c0416 feat: replace SVGs with lucide-react icons across components
- Updated ProfilePage, AuthButton, RecentTasks, WelcomeSection, DesktopControls, MobileControls, and various Kanban components to use lucide-react icons instead of SVGs for improved consistency and maintainability.
- Icons replaced include Check, User, Mail, Calendar, Shield, Save, X, Loader2, Filter, Target, List, Grid3X3, ChevronDown, ChevronRight, Edit, Trash2, and Plus.
2025-10-04 06:25:04 +02:00
Julien Froidefond
e14b428e12 feat: add lucide-react icons to QuickActions component
- Integrated lucide-react icons for QuickActions, replacing SVGs with Plus, LayoutGrid, Calendar, and Settings icons for improved UI consistency.
- Updated package.json and package-lock.json to include lucide-react dependency.
- Marked the Gravatar task as complete in TODO.md for better tracking of UI/UX improvements.
2025-10-03 17:32:34 +02:00
Julien Froidefond
0658b8ff93 feat: update Card components to use variant="glass"
- Changed Card components in various charts and dashboard sections to use the "glass" variant for a consistent UI enhancement.
- This update affects CompletionTrendChart, PriorityDistributionChart, VelocityChart, WeeklyStatsCard, DashboardStats, ProductivityAnalytics, RecentTasks, TagDistributionChart, MetricsDistributionCharts, MetricsMainCharts, CriticalDeadlinesCard, DeadlineRiskCard, DeadlineSummaryCard, and StatCard.
2025-10-03 17:29:46 +02:00
Julien Froidefond
9fb374fb23 feat: update TODO.md with UI/UX issues and feature requests
- Added a comprehensive list of UI/UX problems identified in meetings, including design improvements for homepage cards, Gravatar integration, and various icon enhancements.
- Included feature requests for Jira/TFS integration and activity logging.
- Organized issues into categories for better clarity and tracking.
2025-10-03 17:24:50 +02:00
Julien Froidefond
48e3822696 feat: enhance login page with random theme and background features
- Added RandomThemeApplier to apply a random theme on login.
- Introduced RandomBackground component for setting a random background from presets.
- Updated GlobalKeyboardShortcuts import in RootLayout for consistent keyboard shortcut handling.
- Refactored BackgroundContext to include cycleBackground functionality for dynamic background changes.
- Removed deprecated useBackgroundCycle hook to streamline background management.
2025-10-03 17:11:02 +02:00
Julien Froidefond
aae35aa811 feat: add alignRight prop to IntegrationFilter and update HomePageClient
- Introduced alignRight prop in IntegrationFilter for dropdown alignment control.
- Updated HomePageClient to pass alignRight as true, ensuring consistent dropdown positioning.
2025-10-03 09:21:20 +02:00
Julien Froidefond
943d14cfc1 feat: add discreet info on story points calculation in Jira dashboard
- Included a new info banner in the overview tab explaining the use of story points in Jira.
- The banner provides default values for different task types when story points are not defined.
2025-10-03 09:11:24 +02:00
Julien Froidefond
c84ee86ed4 feat: add maxSyncPeriod configuration to TFS settings
- Introduced maxSyncPeriod option in TfsConfigForm for user-defined synchronization duration.
- Updated TfsService to filter pull requests based on the configured maxSyncPeriod.
- Enhanced TfsPullRequest type to include 'rejected' status for better PR management.
- Set default maxSyncPeriod to '90d' in user preferences and TFS configuration.
2025-10-03 09:06:24 +02:00
Julien Froidefond
7900ba3b73 feat: optimize task handling in PendingTasksSection
- Implemented optimistic UI updates for task archiving and deletion to enhance user experience.
- Added error handling to reload pending tasks in case of failures during task operations.
- Streamlined task state management by filtering out archived or deleted tasks immediately.
2025-10-03 08:45:46 +02:00
Julien Froidefond
1a670cb392 feat: update CriticalDeadlinesCard to use TaskCard and add disableHover prop
- Replaced custom task rendering with TaskCard component for better consistency and maintainability.
- Introduced disableHover prop to control hover effects on task cards.
- Updated DeadlineOverview to pass disableHover prop as true.
2025-10-03 08:44:14 +02:00
Julien Froidefond
1dfb8f8ac1 feat: enhance HomePage with tag metrics and analytics integration
- Added TagAnalyticsService to fetch tag distribution metrics for the HomePage.
- Updated HomePageClient and ProductivityAnalytics components to utilize new tag metrics.
- Refactored TagsClient to use utility functions for color validation and generation.
- Simplified TagForm to use centralized tag colors from TAG_COLORS.
2025-10-03 08:37:43 +02:00
Julien Froidefond
735070dd6f feat: refactor IntegrationFilter for Kanban and Dashboard compatibility
- Updated IntegrationFilter to support both Kanban and Dashboard modes with new filters for manual tasks.
- Replaced SourceQuickFilter with IntegrationFilter in Desktop and Mobile controls for consistency.
- Removed deprecated SourceQuickFilter component to streamline codebase.
- Enhanced task filtering logic to include pinned tasks and manual task visibility.
2025-10-03 08:30:40 +02:00
Julien Froidefond
2137da2ac2 feat: refine TFS scheduler and user-specific configurations
- Enhanced TFS scheduler logic to better manage user-specific settings and preferences.
- Updated API routes for improved handling of user-specific configurations in TFS operations.
- Cleaned up related components to streamline user interactions and ensure accurate task synchronization.
2025-10-03 08:17:48 +02:00
Julien Froidefond
c1de8cd064 feat: update TODO_ARCHIVE and TODO with new features and refactoring notes
- Added completed tasks for background image customization and TFS scheduler integration in TODO_ARCHIVE.
- Cleaned up TODO list by removing completed items related to dark mode and background image features.
- Documented new functionalities and architectural changes for better clarity and tracking.
2025-10-03 08:17:13 +02:00
Julien Froidefond
a1f82a4c9b feat: refactor TFS integration structure and add scheduler functionality
- Updated TFS service imports to a new directory structure for better organization.
- Introduced new API routes for TFS scheduler configuration and status retrieval.
- Implemented TFS scheduler logic to manage automatic synchronization based on user preferences.
- Added components for TFS configuration and scheduler management, enhancing user interaction with TFS settings.
- Removed deprecated TfsSync component, consolidating functionality into the new structure.
2025-10-03 08:15:12 +02:00
Julien Froidefond
f4c6b1181f feat: enhance TFS and Jira field tests with user-specific configurations
- Updated `testJiraFields` and `testStoryPoints` to accept a `userId` from command line arguments, allowing for user-specific Jira configurations.
- Modified TFS sync and test routes to include user authentication checks and pass the logged-in user's ID for task synchronization and connection testing.
- Refactored `TfsService` methods to utilize user-specific configurations, improving flexibility and accuracy in TFS operations.
2025-10-03 07:51:57 +02:00
Julien Froidefond
39936f5d06 feat: enhance Jira scheduler with user-specific handling
- Updated `jiraScheduler` methods to accept a `userId` parameter, allowing for user-specific configurations and status retrieval.
- Modified the `POST` and `GET` routes to pass the current user's ID, ensuring accurate scheduler status and actions based on the logged-in user.
- Adjusted the `JiraSchedulerConfig` component to reflect changes in scheduler activation logic from `isEnabled` to `isRunning`, improving clarity in the UI.
- Enhanced synchronization response structure to provide detailed task statistics for better client-side handling.
2025-10-03 07:48:21 +02:00
Julien Froidefond
775788fdb5 feat: ensure user existence in getOrCreateUserPreferences
- Added upsert logic to check and create a user if they don't exist before fetching user preferences.
- This prevents duplicates and ensures a default user setup with a temporary password.
2025-10-03 07:39:21 +02:00
Julien Froidefond
10c1f811ce feat: integrate ToastProvider and enhance theme management
- Added `ToastProvider` to `RootLayout` for improved user feedback on theme changes.
- Updated `ThemeProvider` to display toast notifications with theme names and icons upon theme changes.
- Refactored theme-related imports to streamline code and improve maintainability.
- Simplified background cycling logic in `useBackgroundCycle` to utilize centralized background definitions.
- Cleaned up unused background definitions in `BackgroundContext` for better clarity and performance.
2025-10-02 17:24:37 +02:00
Julien Froidefond
99377ee38d feat: enhance BackgroundImageSelector with custom image management
- Removed preserved custom URL handling and replaced it with a custom images array for better management of user-added backgrounds.
- Updated the component to allow adding, selecting, and removing custom images, improving user experience and flexibility.
- Adjusted background cycling logic to include custom images, ensuring a seamless integration with existing backgrounds.
2025-10-02 14:40:50 +02:00
Julien Froidefond
fbb9311f9e feat: update background gradients and presets in BackgroundImageSelector
- Adjusted gradient definitions for various themes to enhance visual appeal and consistency.
- Added new gradient presets (sunset, ocean, forest, galaxy) to the BackgroundContext for broader customization options.
- Cleaned up unused console logs in useBackgroundCycle for better performance and readability.
2025-10-02 14:14:06 +02:00
Julien Froidefond
9094aca1ff feat: enhance keyboard shortcuts and background image handling
- Added `GlobalKeyboardShortcuts` component to manage global keyboard shortcuts.
- Introduced new keyboard shortcut (Shift + B) for changing the background.
- Updated `BackgroundImageSelector` to preserve custom background URLs and allow restoration of previously set backgrounds.
- Improved local storage handling for custom backgrounds to enhance user experience.
2025-10-02 13:52:18 +02:00
Julien Froidefond
d4e8dc144b style: update Kanban components for improved background effects
- Adjusted background opacity in `Board` and `SwimlanesBase` components to enhance visual layering.
- Modified `Card` component to support a new `background` prop for better customization of column cards.
- Updated styles for `Card` variants to include new gradient effects and backdrop blur adjustments, improving overall aesthetics.
2025-10-02 13:38:31 +02:00
Julien Froidefond
46c1c5e9a1 feat: add integration filtering to dashboard components
- Introduced `IntegrationFilter` to allow users to filter tasks by selected and hidden sources.
- Updated `DashboardStats`, `ProductivityAnalytics`, `RecentTasks`, and `HomePageContent` to utilize the new filtering logic, enhancing data presentation based on user preferences.
- Implemented filtering logic in `AnalyticsService` and `DeadlineAnalyticsService` to support source-based metrics calculations.
- Enhanced UI components to reflect filtered task data, improving user experience and data relevance.
2025-10-02 13:15:10 +02:00
Julien Froidefond
2e3e8bb222 feat: optimize UserPreferencesContext with debounce and local storage
- Added debounce functionality for kanban filters, view preferences, and column visibility updates to reduce server load.
- Implemented local storage synchronization for immediate updates, ensuring user preferences persist across sessions.
- Removed unnecessary startTransition calls to streamline state updates and improve UI responsiveness.
2025-10-02 12:31:29 +02:00
Julien Froidefond
63ef861360 feat: add isArchived property to DailyCheckbox and related components
- Introduced `isArchived` property to `DailyCheckbox` to track archived tasks.
- Updated `DailyCheckboxItem`, `CheckboxItem`, and `DailySection` components to reflect archived state in UI.
- Adjusted checkbox behavior to disable interactions for archived tasks and visually indicate their status.
- Enhanced task management services to include archived status during task creation and updates.
2025-10-02 11:02:29 +02:00
Julien Froidefond
e0b5afb437 refactor: simplify KanbanFilters and SourceQuickFilter components
- Removed unused imports and state management for dropdowns, enhancing performance and readability.
- Replaced custom dropdown implementation with a reusable `Dropdown` component for better consistency across the UI.
- Updated button styles and logic for clearer user interaction in the filters.
- Integrated dropdowns into the `SourceQuickFilter` for improved functionality and user experience.
2025-10-02 08:32:10 +02:00
Julien Froidefond
7e79dbe49c feat: enhance BackgroundImageSelector with new gradients and custom URL input
- Added new gradient options for background selection, including sunset, ocean, forest, and galaxy themes.
- Updated existing gradient descriptions and previews for clarity.
- Improved custom URL input with enhanced styling and performance tips for better user guidance.
- Reset advanced options when the background image changes to streamline user experience.
2025-10-01 22:54:06 +02:00
Julien Froidefond
ead02e0aaa fix: add pointer-events-none to Card variants
- Updated Card component styles to include `pointer-events-none` for all variants, preventing interaction during background effects. This enhances user experience by ensuring visual elements do not interfere with user actions.
2025-10-01 22:50:49 +02:00
Julien Froidefond
133a09f995 feat: enhance Card and StyledCard components with new shadow and gradient effects
- Added new shadow variables for light, medium, and heavy effects in `globals.css` to improve card depth.
- Updated `Card` and `StyledCard` components to utilize these shadows and introduced gradient backgrounds for a more dynamic appearance.
- Enhanced hover effects in `TaskCard` for improved user interaction with scaling and opacity transitions.
2025-10-01 22:23:29 +02:00
Julien Froidefond
e73e46893f feat: implement personalized background image feature
- Added functionality for users to select and customize background images in settings, including predefined options and URL uploads.
- Updated `ViewPreferences` to store background image settings and modified `userPreferencesService` to handle updates.
- Enhanced global styles for improved readability with background images, including blur and transparency effects.
- Integrated `BackgroundImageSelector` component into settings for intuitive user experience.
- Refactored `Card` components across the app to use a new 'glass' variant for better aesthetics.
2025-10-01 22:15:11 +02:00
Julien Froidefond
988ffbf774 refactor: remove automatic theme synchronization in UserPreferencesContext
- Eliminated automatic synchronization of user preferences with the theme from ThemeContext, simplifying the logic.
- Updated related useEffect hooks to reflect this change, ensuring that ThemeContext remains the source of truth for theme management.
2025-10-01 21:48:54 +02:00
Julien Froidefond
0d20d602cb feat: enhance Card and TaskCard components with gradient backgrounds and hover effects
- Updated `Card` component to include gradient backgrounds for different variants, improving visual depth.
- Modified `TaskCard` hover effects to include scaling and translation for a more dynamic user interaction experience.
2025-10-01 21:43:18 +02:00
Julien Froidefond
a034e265fd feat: update task statistics calculation
- Renamed variables in `getTaskStats` for clarity, changing `completed` to `done` and ensuring `archived` is counted separately.
- Added logic to calculate `completed` tasks as the sum of `done` and `archived`, improving task status reporting.
2025-10-01 21:28:24 +02:00
Julien Froidefond
c104fc0e11 feat: enhance WelcomeSection with animations and particle effects
- Added animation states for welcome message, time message, and greeting to improve user engagement.
- Introduced particle effects for a dynamic background experience.
- Refactored button to trigger message refresh with animations, enhancing interactivity.
- Updated styles for improved visual appeal and responsiveness.
2025-10-01 21:24:45 +02:00
Julien Froidefond
e2527ca88a feat: add primary tag functionality to tasks
- Introduced `primaryTagId` to `Task` model and updated related components to support selecting a primary tag.
- Enhanced `TaskCard`, `EditTaskForm`, and `TagInput` to handle primary tag selection and display.
- Updated `TasksService` to manage primary tag data during task creation and updates.
- Added `emoji-regex` dependency for improved emoji handling in task titles.
2025-10-01 21:11:50 +02:00
Julien Froidefond
014b0269dc fix: nextauth env in docker compose 2025-10-01 14:41:44 +02:00
Julien Froidefond
5b3f705689 feat: update AuthButton and Header for improved user experience
- Increased avatar and icon sizes in `AuthButton` for better visibility.
- Integrated session handling in `Header` to display user profile link and sign-out button when authenticated, enhancing mobile menu functionality.
- Refactored mobile menu overlay to a modal for improved usability.
2025-10-01 13:56:15 +02:00
Julien Froidefond
f13ed5b8d9 feat: integrate ConfirmModal for delete confirmations across components
- Added `ConfirmModal` to `TaskCard`, `JiraConfigForm`, `TfsConfigForm`, and `TagsManagement` for improved user experience during delete actions.
- Replaced direct confirmation prompts with modals, enhancing UI consistency and usability.
- Updated state management to handle modal visibility and confirmation logic effectively.
2025-10-01 13:47:57 +02:00
Julien Froidefond
352a65af47 refactor: remove unused handleAddCheckbox function in FormsSection
- Deleted the `handleAddCheckbox` function from `FormsSection` as it was not being utilized, streamlining the component's code.
2025-10-01 13:41:17 +02:00
Julien Froidefond
7ebf7d491b feat: add filtering options for tags management
- Introduced `showOnlyWithoutIcons` state in `TagsManagement` to filter tags without icons.
- Updated `TagsFilters` component to include a button for toggling the new filter, displaying the count of tags without icons.
- Enhanced filtering logic to accommodate the new option, improving tag management functionality.
2025-10-01 13:40:51 +02:00
Julien Froidefond
4885871657 feat: enhance login and registration pages with session handling
- Added `useEffect` to redirect authenticated users to the home page in both `LoginPage` and `RegisterPage`.
- Integrated `useSession` from `next-auth/react` to manage session state and loading indicators.
- Implemented loading state display while checking session status, improving user experience during authentication.
- Prevented form display for authenticated users, streamlining the login and registration process.
2025-09-30 23:37:37 +02:00
Julien Froidefond
8519ec094f feat: add line clamp utility and integrate RecentTaskTimeline component
- Added a new CSS utility for line clamping to `globals.css` for better text overflow handling.
- Integrated `WelcomeSection` into `HomePageClient` for enhanced user experience.
- Replaced `TaskCard` with `RecentTaskTimeline` in `RecentTasks` for improved task visualization.
- Updated `ui/index.ts` to export `RecentTaskTimeline` and showcased it in `CardsSection` and `FeedbackSection`.
2025-09-30 23:34:03 +02:00
Julien Froidefond
d8ca4ef00b feat: enhance profile page and authentication with user avatar support
- Updated `next.config.ts` to allow images from various external sources, including LinkedIn and GitHub.
- Refactored `ProfilePage` to improve layout and display user avatar, name, and role more prominently.
- Enhanced `AuthButton` to show user avatar if available, improving user experience.
- Updated authentication logic in `auth.ts` to include user avatar and role in session management.
- Extended JWT type definitions to support new user fields (firstName, lastName, avatar, role) for better user data handling.
2025-09-30 23:15:21 +02:00
Julien Froidefond
307b3a8a14 fix: adjust button layout in ButtonsSection for better spacing
- Updated button container in `ButtonsSection` to use `space-x-4` for horizontal spacing between buttons, improving visual alignment and usability across variants, sizes, and states.
2025-09-30 23:06:04 +02:00
Julien Froidefond
703145a791 feat: restructure UI showcase with new sections and components
- Refactored `UIShowcaseClient` to utilize new section components: `ButtonsSection`, `BadgesSection`, `CardsSection`, `FormsSection`, `NavigationSection`, `FeedbackSection`, and `DataDisplaySection`.
- Removed redundant state management and imports, simplifying the component structure.
- Enhanced organization of UI components for improved usability and navigation within the showcase.
2025-09-30 23:04:10 +02:00
Julien Froidefond
785dc91159 feat: add Table of Contents component to UI showcase
- Introduced `TableOfContents` component for improved navigation within the UI showcase.
- Implemented section extraction and intersection observer for active section tracking.
- Updated `UIShowcaseClient` to include the new component, enhancing user experience with a sticky navigation menu.
- Added IDs to sections for better linking and scrolling functionality.
2025-09-30 22:31:57 +02:00
Julien Froidefond
7aa9d6dd6b fix: streamline error handling and clean up unused imports
- Simplified error handling in `LoginPage` by removing the error parameter in the catch block.
- Removed unused import of `cn` in `KeyboardShortcutsModal` to clean up the code.
- Updated `UserPreferencesContext` to only destructure `status` from `useSession`, improving clarity.
- Refactored multiple methods in `UserPreferencesService` to eliminate unnecessary variable assignments, enhancing performance.
- Added ESLint directive to suppress unused variable warning for `NextAuth` import in type definitions.
2025-09-30 22:20:57 +02:00
Julien Froidefond
30aaca4877 feat: enhance user preferences management with userId integration
- Added `userId` field to `UserPreferences` model in Prisma schema for user-specific preferences.
- Implemented migration to populate existing preferences with the first user.
- Updated user preferences service methods to handle user-specific data retrieval and updates.
- Modified API routes and components to ensure user authentication and fetch preferences based on the authenticated user.
- Enhanced session management in various components to load user preferences accordingly.
2025-09-30 22:15:44 +02:00
Julien Froidefond
17b86b6087 feat: add authentication support and user model
- Updated `env.example` to include NextAuth configuration for authentication.
- Added `next-auth` dependency to manage user sessions.
- Introduced `User` model in Prisma schema with fields for user details and password hashing.
- Integrated `AuthProvider` in layout for session management across the app.
- Enhanced `Header` component with `AuthButton` for user authentication controls.
2025-09-30 21:49:52 +02:00
Julien Froidefond
43c141d3cd feat: add additional UI components to UIShowcaseClient
- Integrated new components including TagDisplay, TagInput, DateTimeInput, FormField, LoadingSpinner, PrioritySelector, StatusBadge, KeyboardShortcutsModal, and Modal for enhanced user interaction.
- Organized components into sections for better structure and usability, improving overall UI showcase experience.
2025-09-30 21:23:30 +02:00
Julien Froidefond
f145bed97d feat: integrate TagDisplay component into TaskCard
- Replaced badge rendering with TagDisplay for improved tag visualization.
- Added showDot prop to control dot display alongside tag colors.
2025-09-30 10:26:34 +02:00
Julien Froidefond
884139f8f7 style: update TaskCard badge colors to use CSS variables
- Changed badge text and border colors from hardcoded values to CSS variables for improved theming and consistency.
2025-09-30 10:21:26 +02:00
Julien Froidefond
dc7b7c7616 feat: update TaskCard component to include todosCount in padding logic
- Modified padding logic to account for `todosCount`, ensuring proper spacing when there are todos present.
- Updated footer visibility condition to include `todosCount`, enhancing the display of task metadata based on the presence of todos.
2025-09-30 10:19:34 +02:00
Julien Froidefond
9d63d31064 feat: improve TagInput component with dropdown positioning
- Added logic to calculate and update the dropdown position dynamically based on the input container's position, enhancing the user experience.
- Implemented portal rendering for the suggestions dropdown to avoid z-index issues, ensuring it displays correctly above other elements.
- Refactored the component to use a `containerRef` for better positioning management.
2025-09-30 10:15:02 +02:00
Julien Froidefond
270a2bd4d0 feat: enhance QuickAddTask component with new UI elements
- Replaced input fields with `FormField`, `PrioritySelector`, and `DateTimeInput` for improved user experience and consistency.
- Integrated `LoadingSpinner` to indicate submission state, enhancing feedback during task creation.
- Streamlined state management for form fields, ensuring better data handling.
2025-09-30 10:11:44 +02:00
Julien Froidefond
d1d65cdca1 feat: enhance checkbox update functionality
- Updated `handleUpdateCheckbox` to accept an optional `date` parameter for modifying date/time.
- Adjusted related components (`DailyCheckboxItem`, `DailySection`, `EditCheckboxModal`) to support the new date functionality, improving task management capabilities.
- Added date input field in `EditCheckboxModal` for user interaction with date/time settings.
2025-09-30 10:02:58 +02:00
Julien Froidefond
df7d2a9afa fix: update default option in DailySection component
- Changed default option from "task" to "meeting" in the DailySection input field for improved clarity and functionality.
2025-09-30 08:42:49 +02:00
Julien Froidefond
f50f4baaa9 feat: enhance EditCheckboxModal with new UI components
- Replaced task status and tags display with `StatusBadge` and `TagDisplay` for improved visual clarity.
- Updated task search input to use `SearchInput` for better user experience.
- Refactored task display sections to utilize `Card` component for consistent styling.
2025-09-30 08:41:30 +02:00
Julien Froidefond
f0d14e29f8 feat: enhance task filtering in EditCheckboxModal
- Updated `filteredTasks` logic to exclude tasks marked as "objectif principal" (isPinned = true) for better task management.
- Added `tagDetails` property to `Task` interface to store detailed tag information, improving task data structure.
- Adjusted `TasksService` to extract and include tag details when retrieving tasks from the database.
2025-09-30 08:30:57 +02:00
Julien Froidefond
6ef52bec85 fix: update labels in ManagerWeeklySummary and Header components
- Changed header title in `ManagerWeeklySummary` from "Résumé Manager" to "Weekly" for clarity.
- Updated navigation label in `Header` from "Manager" to "Weekly" to maintain consistency across the application.
2025-09-29 22:38:35 +02:00
Julien Froidefond
c647725536 feat: enhance AchievementCard and ManagerSummaryService logic
- Added logic to differentiate between regular achievements and todos in `AchievementCard`, changing background color accordingly.
- Updated todos count display to only show for non-todo achievements, improving clarity.
- Refactored `ManagerSummaryService` to remove outdated filters and allow unlimited display of accomplishments and challenges, enhancing data visibility.
- Simplified priority handling by including 'urgent' as a high priority, ensuring better task categorization.
2025-09-29 22:37:00 +02:00
Julien Froidefond
1d7c2b5e1a feat: add filter for completed tasks in the last 7 days
- Implemented `showCompletedLast7Days` filter in `KanbanFilters` to toggle visibility of tasks completed in the last week.
- Updated `GeneralFilters` to include a new filter chip for the completed tasks toggle.
- Enhanced `TasksProvider` to filter tasks based on the new criteria, improving task management capabilities.
- Adjusted `FilterSummary` to display the active filter status for better user feedback.
2025-09-29 22:24:03 +02:00
Julien Froidefond
dc46232dd7 style: refactor layout and enhance card UI in WeeklyManager
- Updated `WeeklyManagerPage` layout to use a `<main>` tag for better semantic structure.
- Refined `ManagerWeeklySummary` component to display narrative and metrics side by side, improving visual organization.
- Enhanced `AchievementCard` and `ChallengeCard` styles for better color differentiation and user experience.
- Adjusted spacing and grid layouts for improved responsiveness and clarity in the UI.
2025-09-29 22:04:38 +02:00
Julien Froidefond
bff4f394ac feat: add updatedAt field to AchievementCard and related services
- Introduced `updatedAt` property in `AchievementData`, `KeyAccomplishment`, and `Task` interfaces for improved task tracking.
- Updated `AchievementCard` UI to display the last updated date alongside completion date, enhancing user visibility.
- Adjusted `UIShowcaseClient` and `TasksService` to include `updatedAt` values, ensuring consistency across task management components.
2025-09-29 21:42:09 +02:00
Julien Froidefond
ec6c51f9ec feat: add todosCount to RecentTasks and TaskCard components
- Included `todosCount` prop in `RecentTasks` and `TaskCard` for better task management visibility.
- Updated `TaskCard` UI to display the number of related todos, enhancing user interaction.
- Modified `Task` interface and `TasksService` to support todos count retrieval from the database.
- Added sample `todosCount` values in `UIShowcaseClient` for demonstration purposes.
2025-09-29 21:30:24 +02:00
Julien Froidefond
74c658b3e7 feat: add updatedAt field to TaskType and adjust completedAt logic
- Introduced `updatedAt` property in `TaskType` for better task tracking.
- Modified `completedAt` assignment to use `task.updatedAt` when `completedAt` is undefined, ensuring more accurate task completion timestamps.
2025-09-29 21:22:15 +02:00
Julien Froidefond
32f9d1d5de feat: enhance KanbanPageClient and KeyboardShortcuts with new functionality
- Added `toggleFontSize` and `handleToggleDueDateFilter` to `KanbanPageClient` for improved user control over font size and due date visibility.
- Replaced `useKeyboardShortcuts` with `useGlobalKeyboardShortcuts` for better shortcut management across components.
- Updated keyboard shortcuts in `KeyboardShortcutsContext` to include new actions for toggling objectives, due date filters, and font size.
- Refined `KeyboardShortcutsModal` layout for better usability and consistency.
- Removed deprecated `useKeyboardShortcuts` hook to streamline codebase.
2025-09-29 20:57:00 +02:00
Julien Froidefond
749f69680b feat: integrate global keyboard shortcuts across multiple components
- Added `KeyboardShortcutsProvider` to `RootLayout` for centralized keyboard shortcut management.
- Implemented `useGlobalKeyboardShortcuts` in `DailyPageClient`, `KanbanPageClient`, and `HomePageClient` to enhance navigation and task management with keyboard shortcuts.
- Updated `KeyboardShortcuts` component to render a modal for displaying available shortcuts, improving user accessibility.
- Enhanced `Header` component with buttons to open the keyboard shortcuts modal, streamlining user interaction.
2025-09-29 17:29:11 +02:00
Julien Froidefond
c1a14f9196 feat: enhance JiraDashboardPage with new components and improved UI
- Integrated `PeriodSelector`, `SkeletonGrid`, and `MetricsGrid` for better data visualization and user interaction.
- Replaced legacy period selection and error display with new components for a cleaner UI.
- Updated `UIShowcaseClient` to demonstrate new Jira dashboard components, enhancing showcase functionality.
2025-09-29 16:47:35 +02:00
Julien Froidefond
6c0c353a4e style: update special card colors in globals.css
- Changed `--jira-card` and `--tfs-card` colors to more subtle shades of slate for improved visual consistency.
- Adjusted comments to reflect the new color choices.
2025-09-29 16:25:03 +02:00
Julien Froidefond
3fcada65f6 fix: update todosCount checks and refactor key accomplishments extraction
- Changed todosCount checks in AchievementCard and ChallengeCard to ensure proper handling of undefined values.
- Updated extractKeyAccomplishments method to be asynchronous and count all related todos using Prisma, improving accuracy in task completion metrics.
- Refactored relatedItems and todosCount handling for better clarity and functionality in ManagerSummaryService.
2025-09-29 16:20:35 +02:00
Julien Froidefond
d45a04d347 feat: refactor Daily components and enhance UI integration
- Replaced `DailyCalendar` with a new `Calendar` component for improved functionality and consistency.
- Introduced `AlertBanner` to replace `DeadlineReminder`, providing a more flexible way to display urgent tasks.
- Updated `DailyAddForm` to use new options for task types, enhancing user experience when adding tasks.
- Removed unused state and components, streamlining the DailyPageClient for better performance and maintainability.
- Enhanced `DailySection` to utilize new `CheckboxItem` format for better integration with the UI.
- Cleaned up imports and improved overall structure for better readability.
2025-09-29 09:47:13 +02:00
Julien Froidefond
41fdd0c5b5 style: refine dark theme colors in globals.css
- Updated background and card colors for the dark theme to enhance visual clarity and consistency.
- Adjusted comments for better understanding of color choices.
2025-09-29 08:53:16 +02:00
Julien Froidefond
8d657872c0 refactor: update theme management and enhance UI components
- Refactored theme imports in `preferences.ts` and `ThemeSelector.tsx` to use centralized `theme-config`.
- Added new CSS variables for special cards in `globals.css` to improve theme consistency.
- Enhanced `Header` and `TaskCard` components with theme dropdown functionality for better user experience.
- Updated `ThemeProvider` to support cycling through dark themes, improving theme selection flexibility.
- Cleaned up unused imports and streamlined component structures for better maintainability.
2025-09-29 08:51:20 +02:00
Julien Froidefond
641a009b34 refactor: streamline TaskCard component and enhance UI integration
- Removed unused state and effects in `TaskCard`, simplifying the component structure.
- Integrated `UITaskCard` for improved UI consistency and modularity.
- Updated event handlers for editing and deleting tasks to enhance user interaction.
- Enhanced props handling for better customization and flexibility in task display.
- Improved emoji handling and title editing functionality for a smoother user experience.
2025-09-28 22:36:22 +02:00
Julien Froidefond
687d02ff3a feat: enhance Kanban components with new UI elements
- Added `ColumnHeader`, `EmptyState`, and `DropZone` components to improve the Kanban UI structure and user experience.
- Refactored `KanbanColumn` to utilize the new components, enhancing readability and maintainability.
- Updated `Card` component to support flexible props for shadow, border, and background, allowing for better customization across the application.
- Adjusted `SwimlanesBase` to incorporate the new `ColumnHeader` for consistent column representation.
2025-09-28 22:10:12 +02:00
Julien Froidefond
5a3d825b8e refactor: simplify swimlane mode handling in KanbanFilters
- Removed swimlane mode toggle and dropdown, streamlining the swimlane mode functionality.
- Updated `handleSwimlanesToggle` to cycle through swimlane modes directly, enhancing clarity and usability.
- Cleaned up unused state and refs related to swimlane mode, improving component performance and maintainability.
2025-09-28 21:55:56 +02:00
Julien Froidefond
0fcd4d68c1 feat: unify CardHeader padding across components
- Updated `CardHeader` padding from `pb-3` to `pb-4` in `JiraLogs`, `JiraSync`, `KanbanColumn`, `ObjectivesBoard`, and `DesktopControls` for consistent spacing.
- Refactored `DesktopControls` and `KanbanFilters` to utilize new `ControlPanel`, `ControlSection`, and `ControlGroup` components, enhancing layout structure and maintainability.
- Replaced button elements with `ToggleButton` and `FilterChip` components in various filter sections for improved UI consistency and usability.
2025-09-28 21:53:22 +02:00
Julien Froidefond
bdf8ab9fb4 feat: add new dashboard components and enhance UI
- Introduced new CSS variables for light theme in `globals.css` to improve visual consistency.
- Replaced `Card` component with `StatCard`, `ProgressBar`, and `MetricCard` in `DashboardStats`, `ProductivityAnalytics`, and `RecentTasks` for better modularity and reusability.
- Updated `QuickActions` to use `ActionCard` for a more cohesive design.
- Enhanced `Badge` and `Button` components with new variants for improved styling options.
- Added new UI showcase section in `UIShowcaseClient` to demonstrate the new dashboard components.
2025-09-28 21:22:33 +02:00
Julien Froidefond
0e2eaf1052 feat: improve theme selector and UI components
- Updated `ThemeSelector` to use a new `ThemePreview` component for better theme visualization.
- Refactored button implementation in `ThemeSelector` to utilize the new `Button` component, enhancing consistency.
- Added a UI showcase section in `GeneralSettingsPageClient` to display available UI components with different themes.
- Enhanced `Badge`, `Button`, and `Input` components with new variants and improved styling for better usability and visual appeal.
- Updated CSS variables in `globals.css` for improved contrast and accessibility across themes.
2025-09-28 21:08:48 +02:00
Julien Froidefond
9ef23dbddc feat: enhance theme management and customization options
- Added support for multiple themes (dracula, monokai, nord, gruvbox, tokyo_night, catppuccin, rose_pine, one_dark, material, solarized) in the application.
- Updated `setTheme` function to accept the new `Theme` type, allowing for more flexible theme selection.
- Introduced `ThemeSelector` component in GeneralSettingsPage for user-friendly theme selection.
- Modified `ThemeProvider` to handle user preferred themes and improved theme toggling logic.
- Updated CSS variables in `globals.css` to support new themes, enhancing visual consistency across the app.
2025-09-28 20:47:26 +02:00
Julien Froidefond
7acb2d7e4e Revert "feat: update TODO and enhance design token integration"
This reverts commit aa348a0f82.
2025-09-28 12:10:43 +02:00
Julien Froidefond
aa348a0f82 feat: update TODO and enhance design token integration
- Marked hydration issues and design system tasks as complete in TODO.md, reflecting progress on theme optimization.
- Added documentation for CSS variables in globals.css to guide future color modifications using design tokens.
- Refactored QuickActions component to utilize StatusMessage for better message display and applied design tokens for button styles, improving UI consistency.
2025-09-28 10:21:39 +02:00
Julien Froidefond
b5d6967fcd feat: refactor theme management and enhance color customization
- Cleaned up theme architecture by consolidating CSS variables and removing redundant theme applications, ensuring a single source of truth for theming.
- Implemented a dark mode override and improved color management using CSS variables for better customization.
- Updated various components to utilize new color variables, enhancing maintainability and visual consistency across the application.
- Added detailed tasks in TODO.md for future enhancements related to user preferences and color customization features.
2025-09-28 10:14:25 +02:00
Julien Froidefond
97770917c1 fix: disable hover effect on taskCard
- Removed hover effect on taskCard for improved user experience and consistency in UI interactions.
- Updated TODO_ARCHIVE.md to reflect this change.
2025-09-28 07:30:58 +02:00
Julien Froidefond
58353a0dec feat: refactor service organization and enhance Daily task management
- Restructured service files into dedicated domains (core, analytics, data-management, integrations, task-management) for better organization and maintainability.
- Updated imports across services to reflect new structure, ensuring all references are correct.
- Added new features to the Daily page, including a section for uncompleted tasks, archiving options, and visual indicators for task age, improving task management experience.
2025-09-27 07:17:10 +02:00
Julien Froidefond
986f1732ea fix: update loadPendingTasks logic to include refreshTrigger condition
- Modified the condition in `PendingTasksSection` to reload tasks if `refreshTrigger` changes, ensuring data is refreshed after toggle/delete actions. This improves the accuracy of displayed pending tasks when filters are applied.
2025-09-27 07:12:53 +02:00
Julien Froidefond
b9f801c110 refactor: replace Jira and TFS filters with SourceQuickFilter in Desktop and Mobile controls
- Removed `JiraQuickFilter` and `TfsQuickFilter` components, consolidating functionality into `SourceQuickFilter`.
- Updated UI sections in `DesktopControls` and `MobileControls` to reflect the new filter structure, enhancing maintainability and reducing redundancy.
2025-09-26 15:05:39 +02:00
Julien Froidefond
6fccf20581 fix: clean up FilterBar title and improve useJiraFilters dependencies
- Simplified the title prop in `FilterBar` for better readability.
- Updated dependency array in `useJiraFilters` to include `filterAnalyticsLocally`, ensuring proper effect execution.
- Added new line at the end of `test-jira-fields.ts` and `test-story-points.ts` for consistency.
2025-09-26 14:00:41 +02:00
Julien Froidefond
7de060566f feat: enhance Jira filters and dashboard functionality
- Added new test scripts in `package.json` for story points and Jira fields validation.
- Updated `JiraDashboardPageClient` to utilize raw analytics for filtering, improving data handling with active filters.
- Introduced a loading state in `FilterBar` with visual feedback for filter application, enhancing user experience.
- Refactored `useJiraFilters` to support local filtering based on initial analytics, streamlining filter management.
- Enhanced `JiraAnalyticsService` to calculate story points based on issue types, improving accuracy in analytics.
2025-09-26 11:54:41 +02:00
Julien Froidefond
bd7ede412e feat: add cache monitoring scripts and enhance JiraAnalyticsCache
- Introduced `cache-monitor.ts` for real-time cache monitoring, providing stats and actions for managing Jira analytics cache.
- Updated `package.json` with new cache-related scripts for easy access.
- Enhanced `JiraAnalyticsCacheService` to support TTL for cache entries, automatic cleanup of expired entries, and improved logging for cache operations.
- Added methods for calculating time until expiry and formatting TTL for better visibility.
2025-09-26 11:42:18 +02:00
Julien Froidefond
350dbe6479 feat: enhance JiraDashboard with initial analytics support
- Updated `JiraDashboardPageClient` to accept `initialAnalytics`, allowing for server-side analytics retrieval.
- Modified `useJiraAnalytics` to initialize state with `initialAnalytics`, improving data handling.
- Adjusted `CollaborationMatrix` to manage client-side rendering and analytics data processing, preventing hydration errors.
- Enhanced `page.tsx` to fetch analytics based on Jira configuration, ensuring data is available for the dashboard.
2025-09-26 11:42:08 +02:00
Julien Froidefond
b87fa64d4d feat: implement optimistic UI for checkbox toggling in DailyCheckboxItem
- Added optimistic state handling in `DailyCheckboxItem` for immediate feedback on checkbox toggles, improving user experience.
- Updated `useDaily` hook to handle checkbox state updates without blocking UI, ensuring smoother interactions.
- Enhanced error handling to rollback state on toggle failures, maintaining data integrity.
2025-09-26 11:32:22 +02:00
Julien Froidefond
a01c0d83d0 style: adjust KanbanFilters layout for improved responsiveness
- Modified the grid layout in `KanbanFilters` to use specific column widths, enhancing the overall UI structure.
- This change optimizes the display of filters, ensuring better alignment and usability across different screen sizes.
2025-09-26 11:20:38 +02:00
Julien Froidefond
31541a11d4 refactor: switch from filteredTasks to regularTasks in filter components
- Updated `JiraFilters`, `PriorityFilters`, `TagFilters`, and `TfsFilters` to use `regularTasks` instead of `filteredTasks` for task counts and available options.
- This change ensures that all tasks are considered, improving the accuracy of project and type availability across filters.
- Adjusted related logic and comments for clarity and consistency.
2025-09-26 11:19:07 +02:00
Julien Froidefond
908f39bc5f feat: add project and type counters to Jira and TFS filters
- Implemented `jiraProjectCounts` and `jiraTypeCounts` in `JiraFilters` to display task counts per project and type, enhancing user visibility.
- Added similar functionality with `tfsProjectCounts` in `TfsFilters`, allowing users to see task distribution across TFS projects.
- Updated UI to show these counts next to project and type labels for better context.
2025-09-26 08:57:08 +02:00
Julien Froidefond
0253555fa4 refactor: update filters to use filteredTasks instead of regularTasks
- Modified `JiraFilters`, `PriorityFilters`, `TagFilters`, and `TfsFilters` components to utilize `filteredTasks` for better accuracy in task filtering.
- Adjusted logic to ensure that available projects, types, and counts are based on the currently filtered tasks, enhancing the relevance of displayed options.
2025-09-26 08:54:15 +02:00
Julien Froidefond
2e9cc4e667 feat: add search functionality and due date filter toggle in DesktopControls
- Implemented a debounced search input for filtering tasks, enhancing user experience with smooth input handling.
- Added a toggle button for filtering tasks by due date, improving task visibility options.
- Updated layout for better responsiveness and integrated new input components for a cleaner UI.
2025-09-26 08:44:05 +02:00
Julien Froidefond
a5199a8302 refactor: update Kanban component imports and streamline filters
- Replaced direct imports of `KanbanFilters` with type imports from `@/lib/types` across multiple components for consistency.
- Simplified `KanbanPageClient` by integrating `DesktopControls` for better organization and readability, removing redundant desktop control code.
- Ensured `compactView` is explicitly typed as boolean in relevant components to enhance type safety.
2025-09-26 08:32:07 +02:00
Julien Froidefond
c224c644b1 refactor: remove unused collapse icon from ObjectivesBoard
- Deleted the collapse icon SVG from the ObjectivesBoard component to clean up the code.
- This change simplifies the button layout and improves readability.
2025-09-26 08:32:00 +02:00
Julien Froidefond
65a307c8ac feat: enhance EditCheckboxModal with task tags display
- Updated the task status display to include tags in a flex container for better layout.
- Added logic to show up to 3 tags with a count for additional tags, improving task information visibility.
2025-09-26 08:17:01 +02:00
Julien Froidefond
a3a5be96a2 style: update text color in BackupTimelineChart for better visibility
- Changed error message text color from gray-500 to gray-600 for improved contrast.
- Updated labels in the backup stats section to use gray-700 for better readability in both light and dark modes.
2025-09-26 08:15:25 +02:00
Julien Froidefond
026a175681 feat: enhance RecentTasks component with task link and date formatting
- Wrapped the task updated date in a flex container for better layout.
- Added a link to the Kanban page for each task, allowing users to quickly access task details directly from the RecentTasks component.
2025-09-26 08:09:08 +02:00
Julien Froidefond
4e9d06896d feat: enhance Kanban navigation and task editing
- Updated `KanbanPageClient` to retrieve `taskId` from URL search parameters for direct task editing.
- Modified links in `DailyCheckboxItem` and `PendingTasksSection` to navigate to the Kanban page with the corresponding `taskId`, improving user experience by allowing quick access to task details.
- Added logic in `KanbanBoardContainer` to automatically open the edit modal if a `taskId` is present, streamlining the editing process.
2025-09-25 22:39:21 +02:00
Julien Froidefond
6ca24b9509 fix: clean up unused imports in KanbanFilters and backup
- Removed unused `getToday` import from `backup.ts` to streamline the code.
- Cleaned up imports in `KanbanFilters.tsx` by removing `useMemo`, which was not utilized, enhancing readability.
2025-09-25 22:35:51 +02:00
Julien Froidefond
b0e7a60308 feat: refactor KanbanFilters to use modular filter components
- Replaced inline priority and tag filtering logic with dedicated `PriorityFilters`, `TagFilters`, `GeneralFilters`, and `ColumnFilters` components for better organization and maintainability.
- Optimized layout to enhance responsiveness and user experience by restructuring the filter display into a grid format.
- Removed unused code related to previous filtering logic, streamlining the component.
2025-09-25 22:33:11 +02:00
Julien Froidefond
f2b18e4527 feat: implement backup management features
- Added `createBackupAction`, `verifyDatabaseAction`, and `refreshBackupStatsAction` for handling backup operations.
- Introduced `getBackupStats` method in `BackupClient` to retrieve daily backup statistics.
- Updated API route to support fetching backup stats.
- Integrated backup stats into the `BackupSettingsPage` and visualized them with `BackupTimelineChart`.
- Enhanced `BackupSettingsPageClient` to manage backup stats and actions more effectively.
2025-09-25 22:28:17 +02:00
Julien Froidefond
cd71824cc8 style: refine button styles and layout in KanbanFilters
- Changed button padding and layout from grid to flex for better responsiveness.
- Adjusted gap sizes for a more compact design.
- Ensured consistent styling across priority and tag buttons for improved UI coherence.
2025-09-25 22:10:00 +02:00
Julien Froidefond
551279efcb feat: add due date filter to KanbanFilters
- Introduced `showWithDueDate` option in `KanbanFilters` to filter tasks based on due dates.
- Added toggle button in the UI for users to easily enable/disable this filter.
- Updated `TasksContext` to handle the new filter state and applied filtering logic in task retrieval.
- Ensured user preferences are saved with the new filter option in `user-preferences.ts`.
2025-09-25 21:44:08 +02:00
Julien Froidefond
a870f7f3dc feat: add initial pending tasks support in DailyPage
- Updated `DailyPageClient` to accept and pass `initialPendingTasks` to the `PendingTasksSection`.
- Modified `page.tsx` to fetch pending tasks from the service and handle graceful fallbacks.
- Adjusted `PendingTasksSection` to initialize state with `initialPendingTasks` and prevent unnecessary loading when initial data is present.
2025-09-25 21:36:13 +02:00
Julien Froidefond
0f22ae7019 fix: update task filtering and layout in ObjectivesBoard
- Removed 'freeze' status from in-progress tasks filtering to improve accuracy.
- Added a new column for 'freeze' tasks, enhancing task visibility and organization on the board.
- Adjusted grid layout to accommodate the new column, ensuring a balanced display.
2025-09-25 09:18:16 +02:00
Julien Froidefond
9ec775acbf fix: enhance TaskCard opacity handling for task statuses
- Updated opacity logic in `TaskCard` to include 'archived' status alongside 'done', improving visual feedback for completed tasks.
- Added specific styling for 'freeze' status to differentiate it visually, enhancing user experience and clarity in task representation.
2025-09-25 08:58:33 +02:00
Julien Froidefond
cff9ad10f0 fix: update task status filtering in ObjectivesBoard
- Modified task filtering logic to include 'freeze' status in in-progress tasks and 'archived' status in completed tasks, enhancing task categorization and improving board accuracy.
2025-09-25 08:31:47 +02:00
Julien Froidefond
6db5e2ef00 fix: ensure default search value in KanbanFilters
- Updated `setKanbanFilters` to set a default empty string for the search filter when no value is provided, preventing potential undefined behavior and improving filter consistency.
2025-09-24 14:03:19 +02:00
Julien Froidefond
167f90369b feat: enhance date handling in TaskBasicFields and date-utils
- Integrated `ensureDate` and `formatDateForDateTimeInput` in `TaskBasicFields` for improved due date management.
- Updated `date-utils` functions to accept both `Date` and `string` types, ensuring robust date validation and parsing.
- Added `ensureDate` utility to handle various date inputs, improving error handling and consistency across date-related functions.
2025-09-24 13:53:18 +02:00
Julien Froidefond
75aa60cb83 style: update DeadlineReminder component styles
- Refactored styles in `DeadlineReminder` for improved visual consistency and clarity.
- Changed card structure and applied new background and border colors using CSS color-mix for better aesthetics.
- Simplified text formatting and ensured proper opacity settings for better readability.
2025-09-24 08:22:16 +02:00
Julien Froidefond
ea21df13c7 fix: improve local search synchronization in KanbanFilters
- Added a ref to track user typing state to prevent overwriting local search when filters change externally.
- Ensured local search updates only occur when the user is not actively typing, enhancing user experience and reducing unnecessary updates.
2025-09-24 08:21:56 +02:00
Julien Froidefond
9c8d19fb09 feat: implement debounced search functionality in KanbanFilters
- Added local state for search input to improve user experience with immediate feedback.
- Introduced a debounced search function to optimize filter updates, reducing unnecessary renders.
- Ensured synchronization of local search state with external filter changes and cleaned up timeouts on component unmount.
2025-09-24 08:11:46 +02:00
Julien Froidefond
7ebc0af3c7 feat: expand multi-tenant architecture and role management in TODO
- Updated migration plan to include a complete user model with roles (ADMIN, MANAGER, USER) and hierarchical relationships.
- Added detailed phases for implementing role-based permissions and collaborative features, enhancing user management and task assignment.
- Structured UI/UX considerations for different user roles, ensuring tailored experiences and improved navigation.
2025-09-24 06:13:48 +02:00
Julien Froidefond
11ebe5cd00 refactor: remove unused analytics actions and integrate metrics directly
- Deleted `analytics.ts` and `deadline-analytics.ts` as they were no longer needed.
- Integrated `AnalyticsService` and `DeadlineAnalyticsService` directly into `HomePage` and `DailyPage`, streamlining data fetching.
- Updated components to utilize the new metrics structure, ensuring proper data flow and rendering.
2025-09-23 22:07:52 +02:00
Julien Froidefond
21e1f68921 fix: clean up imports and improve text formatting
- Removed unused `DeadlineMetrics` import from `deadline-analytics.ts`.
- Updated text in `DeadlineReminder` component to use HTML entity for apostrophe, enhancing rendering consistency.
2025-09-23 21:55:02 +02:00
Julien Froidefond
8a227aec36 feat: update analytics services for improved task handling
- Removed unused `parseDate` import from `analytics.ts`.
- Refactored `ManagerSummaryService` to handle standalone todos with a new priority rule, ensuring todos without tasks default to low priority.
- Updated logic in `MetricsService` to calculate total tasks by including in-progress tasks, enhancing completion rate accuracy.
- Adjusted comments for clarity on new functionality and priority determination.
2025-09-23 21:54:55 +02:00
Julien Froidefond
7ac961f6c7 feat: add DeadlineReminder component for urgent task notifications
- Introduced `DeadlineReminder` component to display urgent tasks based on deadlines.
- Integrated the component into `DailyPageClient` for desktop view, enhancing user awareness of critical tasks.
- Implemented logic to fetch and sort urgent tasks by urgency level and remaining days.
2025-09-23 21:52:56 +02:00
Julien Froidefond
34b9aff6e7 fix: light mode : review some styles 2025-09-23 21:36:50 +02:00
Julien Froidefond
fd3827214f feat: update dashboard components and analytics for 7-day summaries
- Modified `ManagerWeeklySummary`, `MetricsTab`, and `ProductivityAnalytics` to reflect a focus on the last 7 days instead of the current week.
- Enhanced `ManagerSummaryService` and `MetricsService` to calculate metrics over a sliding 7-day window, improving data relevance.
- Added a new utility function `formatDistanceToNow` for better date formatting in French.
- Updated comments and documentation to clarify changes in timeframes.
2025-09-23 21:22:59 +02:00
Julien Froidefond
336b5c1006 feat: integrate Jira and TFS filters into KanbanFilters
- Replaced existing Jira and TFS toggle handlers with `JiraFilters` and `TfsFilters` components for improved modularity and maintainability.
- Streamlined filter management by encapsulating logic within dedicated components, enhancing readability and future extensibility.
2025-09-23 20:53:04 +02:00
Julien Froidefond
db8ff88a4c feat: add TFS filters and integration
- Introduced TFS filtering capabilities in `KanbanFilters` with options to show/hide TFS tasks and filter by TFS projects.
- Integrated `TfsQuickFilter` component into `KanbanPageClient` and `MobileControls` for enhanced task management.
- Updated `TasksContext` to support new TFS filter states and ensure proper task filtering based on TFS criteria.
- Enhanced type definitions in `types.ts` to accommodate new TFS filter properties.
2025-09-23 11:07:24 +02:00
Julien Froidefond
f9c92f9efd doc: todo.md completion 2025-09-23 10:45:57 +02:00
Julien Froidefond
bbb4e543c4 feat: enhance type organization and import structure
- Added detailed tasks in `TODO.md` for isolating and organizing types/interfaces across various services, including analytics, task management, and integrations.
- Updated imports in multiple files to use the new `@/services/core/database` path for consistency.
- Ensured all type imports are converted to `import type { ... }` where applicable for better clarity and performance.
2025-09-23 10:35:52 +02:00
Julien Froidefond
88ab8c9334 feat: complete Phase 5 of service refactoring
- Marked tasks in `TODO.md` as completed for moving TFS and Jira services to the `integrations` directory and correcting imports across the codebase.
- Updated imports in various action files, API routes, and components to reflect the new structure.
- Removed obsolete `jira-advanced-filters.ts`, `jira-analytics.ts`, `jira-analytics-cache.ts`, `jira-anomaly-detection.ts`, `jira-scheduler.ts`, `jira.ts`, and `tfs.ts` files to streamline the codebase.
- Added new tasks in `TODO.md` for future cleaning and organization of service imports.
2025-09-23 10:32:25 +02:00
Julien Froidefond
f5417040fd feat: complete Phase 4 of service refactoring
- Marked tasks in `TODO.md` as completed for moving task-related files to the `task-management` directory and correcting imports across the codebase.
- Updated imports in `seed-data.ts`, `seed-tags.ts`, API routes, and various components to reflect the new structure.
- Removed obsolete `daily.ts`, `tags.ts`, and `tasks.ts` files to streamline the codebase.
- Added new tasks in `TODO.md` for future cleaning and organization of service imports.
2025-09-23 10:25:41 +02:00
Julien Froidefond
b8e0307f03 feat: complete Phase 3 of service refactoring
- Marked tasks in `TODO.md` as completed for moving backup-related files to the `data-management` directory and correcting imports across the codebase.
- Updated imports in `backup-manager.ts`, API routes, and various components to reflect the new structure.
- Removed obsolete `backup.ts` and `backup-scheduler.ts` files to streamline the codebase.
- Added new tasks in `TODO.md` for future cleaning and organization of service imports.
2025-09-23 10:20:56 +02:00
Julien Froidefond
ed16e2bb80 feat: complete Phase 2 of service refactoring
- Marked tasks in `TODO.md` as completed for moving analytics-related files to the `analytics` directory and correcting imports across the codebase.
- Updated imports in `src/actions/analytics.ts`, `src/actions/metrics.ts`, and various components to reflect the new structure.
- Removed unused `analytics.ts`, `manager-summary.ts`, and `metrics.ts` files to streamline the codebase.
2025-09-23 10:15:13 +02:00
Julien Froidefond
f88954bf81 feat: refactor service organization and update imports
- Introduced a new structure for services in `src/services/` to improve organization by domain, including core, analytics, data management, integrations, and task management.
- Moved relevant files to their new locations and updated all internal and external imports accordingly.
- Updated `TODO.md` to reflect the new service organization and outlined phases for further refactoring.
2025-09-23 10:10:34 +02:00
Julien Froidefond
ee64fe2ff3 chore : remove unused methods 2025-09-23 08:30:25 +02:00
Julien Froidefond
e36291a552 chore: Unused package and entire files 2025-09-23 08:21:53 +02:00
Julien Froidefond
723a44df32 feat: TFS Sync 2025-09-22 21:51:12 +02:00
Julien Froidefond
472135a97f fix: remove tooltip functionality from TaskCard component
- Disabled hover tooltip on task cards by removing related state and event handlers.
- Updated TODO.md to reflect the completion of disabling hover on task cards.
2025-09-22 09:09:50 +02:00
Julien Froidefond
b5d53ef0f1 feat: add "Move to Today" functionality for pending tasks
- Implemented a new button in the `PendingTasksSection` to move unchecked tasks to today's date.
- Created `moveCheckboxToToday` action in `daily.ts` to handle the logic for moving tasks.
- Updated `DailyPageClient` and `PendingTasksSection` to integrate the new functionality and refresh the daily view after moving tasks.
- Marked the feature as completed in `TODO.md`.
2025-09-22 08:51:59 +02:00
Julien Froidefond
f9d0641d77 fix: improve text truncation in EditCheckboxModal
- Added `min-w-0` to the title container to prevent overflow in the `EditCheckboxModal`.
- Updated task title and description elements to use `truncate` for better text handling and prevent layout issues.
2025-09-22 08:49:47 +02:00
Julien Froidefond
361fc0eaac feat: enhance mobile and desktop layouts in Daily and Kanban pages
- Refactored `DailyPageClient` to prioritize mobile layout with today's section first and calendar at the bottom for better usability.
- Updated `KanbanPageClient` to include responsive controls for mobile, improving task management experience.
- Adjusted `DailyCheckboxItem` and `DailySection` for better touch targets and responsive design.
- Cleaned up `TODO.md` to reflect changes in mobile interface considerations and task management features.
2025-09-21 21:37:30 +02:00
Julien Froidefond
2194744eef chore: clean up TODO.md by removing outdated mobile component examples
- Deleted specific mobile component examples that are no longer relevant to the current project scope.
- Updated UX considerations for mobile to focus on simplicity and touch optimization.
2025-09-21 21:13:06 +02:00
Julien Froidefond
8be5cb6f70 feat: update TODO.md with completed tasks and new features
- Marked the "Pending Tasks Section" and "Archived Status" as implemented with detailed descriptions.
- Added visual indicators for task age and actions for each task in the Daily page.
- Updated mobile task management features to improve navigation and usability.
2025-09-21 19:58:23 +02:00
Julien Froidefond
3cfed60f43 feat: refactor daily task management with new pending tasks section
- Added `PendingTasksSection` to `DailyPageClient` for displaying uncompleted tasks.
- Implemented `getPendingCheckboxes` method in `DailyClient` and `DailyService` to fetch pending tasks.
- Introduced `getDaysAgo` utility function for calculating elapsed days since a date.
- Updated `TODO.md` to reflect the new task management features and adjustments.
- Cleaned up and organized folder structure to align with Next.js 13+ best practices.
2025-09-21 19:55:04 +02:00
Julien Froidefond
0a03e40469 feat: enhance metrics dashboard with new components and data handling
- Introduced `MetricsOverview`, `MetricsMainCharts`, `MetricsDistributionCharts`, `MetricsVelocitySection`, and `MetricsProductivitySection` for improved metrics visualization.
- Updated `MetricsTab` to integrate new components and streamline data presentation.
- Added compatibility fields in `JiraTask` and `AssigneeDistribution` for better data handling.
- Refactored `calculateAssigneeDistribution` to include a count for total issues.
- Enhanced `JiraAnalyticsService` and `JiraAdvancedFiltersService` to support new metrics calculations.
- Cleaned up unused imports and components for a more maintainable codebase.
2025-09-21 15:55:11 +02:00
Julien Froidefond
c650c67627 feat: integrate UserPreferencesContext for improved preference management
- Added `UserPreferencesProvider` to `RootLayout` for centralized user preferences handling.
- Updated components to remove direct user preferences fetching, relying on context instead.
- Enhanced SSR data fetching by consolidating user preferences retrieval into a single service call.
- Cleaned up unused props in various components to streamline the codebase.
2025-09-21 15:03:19 +02:00
Julien Froidefond
4ba6ba2c0b refactor: unify date handling with utility functions
- Replaced direct date manipulations with utility functions like `getToday`, `parseDate`, and `createDateFromParts` across various components and services for consistency.
- Updated date initialization in `JiraAnalyticsService`, `BackupService`, and `DailyClient` to improve clarity and maintainability.
- Enhanced date parsing in forms and API routes to ensure proper handling of date strings.
2025-09-21 13:04:34 +02:00
Julien Froidefond
c3c1d24fa2 refactor: enhance date handling across components
- Replaced direct date manipulations with utility functions for consistency and readability.
- Updated date formatting in `DailyCalendar`, `RecentTasks`, `CompletionRateChart`, and other components to use `formatDateShort` and `formatDateForDisplay`.
- Improved date parsing in `JiraLogs`, `JiraSchedulerConfig`, and `BackupSettingsPageClient` to ensure proper handling of date strings.
- Streamlined date initialization in `useDaily` and `DailyService` to utilize `getToday` and `getYesterday` for better clarity.
2025-09-21 12:02:06 +02:00
Julien Froidefond
557cdebc13 refactor: date utils and all calls 2025-09-21 11:41:17 +02:00
Julien Froidefond
799a21df5c feat: implement Jira auto-sync scheduler and UI configuration
- Added `jiraAutoSync` and `jiraSyncInterval` fields to user preferences for scheduler configuration.
- Created `JiraScheduler` service to manage automatic synchronization with Jira based on user settings.
- Updated API route to handle scheduler actions and configuration updates.
- Introduced `JiraSchedulerConfig` component for user interface to control scheduler settings.
- Enhanced `TODO.md` to reflect completed tasks related to Jira synchronization features.
2025-09-21 11:30:41 +02:00
Julien Froidefond
a0e2a78372 feat: update Daily and Jira dashboard pages with dynamic titles and improved UI
- Implemented `getTodayTitle` and `getYesterdayTitle` functions in `DailyPageClient` to dynamically set section titles based on the current date.
- Updated `TODO.md` to mark completed tasks related to the Jira dashboard UI consistency.
- Enhanced card content in `JiraDashboardPageClient` to ensure charts are responsive and maintain consistent styling.
- Removed unused date formatting function in `DailySection` for cleaner code.
2025-09-21 10:49:39 +02:00
Julien Froidefond
4152b0bdfc chore: refactor project structure and clean up unused components
- Updated `TODO.md` to reflect new testing tasks and final structure expectations.
- Simplified TypeScript path mappings in `tsconfig.json` for better clarity.
- Revised business logic separation rules in `.cursor/rules` to align with new directory structure.
- Deleted unused client components and services to streamline the codebase.
- Adjusted import paths in scripts to match the new structure.
2025-09-21 10:26:35 +02:00
Julien Froidefond
9dc1fafa76 feat: expand TODO.md with multi-user and mobile interface plans
- Added detailed sections for transitioning to a multi-tenant architecture, including authentication, data model adjustments, and service modifications.
- Introduced a comprehensive migration plan for user data isolation and security considerations.
- Outlined phases for developing a dedicated mobile interface, addressing current usability issues and enhancing user experience on mobile devices.
- Included specific tasks for mobile components and UX optimizations.
2025-09-21 10:12:54 +02:00
Julien Froidefond
d7140507e5 chore: update TODO.md with new feature ideas and refactoring plans
- Added sections for future features including TFS/Azure DevOps integration, task management, and modular architecture.
- Detailed a migration plan for restructuring the project directory to align with Next.js 13+ best practices.
- Included specific tasks for improving integration interfaces and enhancing the user experience.
2025-09-21 09:14:52 +02:00
Julien Froidefond
43998425e6 feat: enhance backup functionality and logging
- Updated `createBackup` method to accept a `force` parameter, allowing backups to be created even if no changes are detected.
- Added user alerts in `AdvancedSettingsPageClient` and `BackupSettingsPageClient` for backup status feedback.
- Implemented `getBackupLogs` method in `BackupService` to retrieve backup logs, with a new API route for accessing logs.
- Enhanced UI in `BackupSettingsPageClient` to display backup logs and provide a refresh option.
- Updated `BackupManagerCLI` to support forced backups via command line.
2025-09-21 07:27:23 +02:00
Julien Froidefond
618e774a30 fix: update database path in README and remove backup link
- Changed `DATABASE_URL` in `data/README.md` to use a relative path for better compatibility.
- Removed the reference to `BACKUP.md` in `DOCKER.md` as it is no longer relevant.
2025-09-21 07:18:49 +02:00
Julien Froidefond
c5bfcc50f8 fix: refine loading states in MetricsTab
- Simplified loading logic by removing unnecessary trends loading check.
- Enhanced UI feedback by disabling the weeks selection during trends loading and added a loading state for the trends chart.
- Improved user experience by displaying a message when no velocity data is available.
2025-09-21 06:42:26 +02:00
Julien Froidefond
6e2b0abc8d chore: update TODO list with new tasks
- Added tasks for backup changes, date refactoring, and component splitting to the TODO.md file.
2025-09-21 06:42:18 +02:00
Julien Froidefond
9da824993d fix: update fs import in SystemInfoService for eslint compliance
- Changed the fs import in `system-info.ts` to comply with eslint rules by adding a comment to disable the specific linting error.
2025-09-21 06:36:13 +02:00
Julien Froidefond
e88b1aad32 chore: remove unused system info functionality
- Deleted `system-info.ts` as it is no longer needed in the codebase.
- No changes made to `workday-utils.ts`, just added a new line for consistency.
2025-09-21 06:34:43 +02:00
Julien Froidefond
3c20df95d9 feat: add system info and backup functionalities to settings page
- Integrated system info fetching in `SettingsPage` for improved user insights.
- Enhanced `SettingsIndexPageClient` with manual backup creation and Jira connection testing features.
- Added loading states and auto-dismiss messages for user feedback during actions.
- Updated UI to display system info and backup statistics dynamically.
2025-09-20 16:38:33 +02:00
Julien Froidefond
da0565472d feat: enhance settings and backup functionality
- Updated status descriptions in `SettingsIndexPageClient` to reflect current functionality.
- Added a new backup management section in settings for better user access.
- Modified `BackupService` to include backup type in filenames, improving clarity and organization.
- Enhanced backup file parsing to support both new and old filename formats, ensuring backward compatibility.
2025-09-20 16:21:50 +02:00
Julien Froidefond
9a33d1ee48 fix: improve date formatting and backup path handling
- Updated `formatTimeAgo` in `AdvancedSettingsPageClient` to use a fixed format for hydration consistency.
- Refined `formatDate` in `BackupSettingsPageClient` for consistent server/client formatting.
- Refactored `BackupService` to use `getCurrentBackupPath` for all backup path references, ensuring up-to-date paths and avoiding caching issues.
- Added `getCurrentBackupPath` method to dynamically retrieve the current backup path based on environment variables.
2025-09-20 16:12:01 +02:00
Julien Froidefond
ee442de773 chore: refine database and backup paths in configuration
- Updated `.gitignore` to only exclude `.db` files in the `/data/` directory and preserve backups.
- Modified `docker-compose.yml` to switch database and backup paths to `dev.db`, aligning with the current development setup.
2025-09-20 15:58:53 +02:00
Julien Froidefond
329018161c chore: update backup configurations and directory structure
- Modified `.gitignore` to exclude all files in the `/data/` directory.
- Enhanced `BACKUP.md` with customization options for backup storage paths and updated database path configurations.
- Updated `docker-compose.yml` to reflect new paths for database and backup storage.
- Adjusted `Dockerfile` to create a dedicated backups directory.
- Refactored `BackupService` to utilize environment variables for backup paths, improving flexibility and reliability.
- Deleted `dev.db` as it is no longer needed in the repository.
2025-09-20 15:45:56 +02:00
Julien Froidefond
dfa8d34855 feat: add workday utility functions
- Introduced utility functions for workday calculations in `workday-utils.ts`, including `getPreviousWorkday`, `getNextWorkday`, `isWorkday`, and `getDayName`.
- Updated `DailyService` and `DailyPageClient` to utilize `getPreviousWorkday` for accurate date handling instead of simple date subtraction.
2025-09-20 15:43:38 +02:00
448 changed files with 61183 additions and 27108 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -9,7 +9,7 @@ description: Enforce business logic separation between frontend and backend
All business logic, data processing, and domain rules MUST be implemented in the backend services layer. The frontend is purely for presentation and user interaction.
## ✅ ALLOWED in Frontend ([components/](mdc:components/), [hooks/](mdc:hooks/), [clients/](mdc:clients/))
## ✅ ALLOWED in Frontend ([src/components/](mdc:src/components/), [src/hooks/](mdc:src/hooks/), [src/clients/](mdc:src/clients/))
### Components
- UI rendering and presentation logic
@@ -73,7 +73,7 @@ const calculateTeamVelocity = (sprints) => {
// This belongs in services/team-analytics.ts
```
## ✅ REQUIRED in Backend ([services/](mdc:services/), [app/api/](mdc:app/api/))
## ✅ REQUIRED in Backend ([src/services/](mdc:src/services/), [src/app/api/](mdc:src/app/api/))
### Services Layer
- All business rules and domain logic

View File

@@ -1,10 +1,10 @@
---
globs: components/**/*.tsx
globs: src/components/**/*.tsx
---
# Components Rules
1. UI components MUST be in components/ui/
1. UI components MUST be in src/components/ui/
2. Feature components MUST be in their feature folder
3. Components MUST use clients for data fetching
4. Components MUST be properly typed

View File

@@ -0,0 +1,167 @@
---
alwaysApply: true
description: CSS Variables theme system best practices
---
# CSS Variables Theme System
## Core Principle: Pure CSS Variables for Theming
This project uses **CSS Variables exclusively** for theming. No Tailwind `dark:` classes or conditional CSS classes.
## ✅ Architecture Pattern
### CSS Structure
```css
:root {
/* Light theme (default values) */
--background: #f1f5f9;
--foreground: #0f172a;
--primary: #0891b2;
--success: #059669;
--destructive: #dc2626;
--accent: #d97706;
--purple: #8b5cf6;
--yellow: #eab308;
--green: #059669;
--blue: #2563eb;
--gray: #6b7280;
--gray-light: #e5e7eb;
}
.dark {
/* Dark theme (override values) */
--background: #1e293b;
--foreground: #f1f5f9;
--primary: #06b6d4;
--success: #10b981;
--destructive: #ef4444;
--accent: #f59e0b;
--purple: #8b5cf6;
--yellow: #eab308;
--green: #10b981;
--blue: #3b82f6;
--gray: #9ca3af;
--gray-light: #374151;
}
```
### Theme Application
- **Single source of truth**: [ThemeContext.tsx](mdc:src/contexts/ThemeContext.tsx) applies theme class to `document.documentElement`
- **No duplication**: Theme is applied only once, not in multiple places
- **SSR safe**: Initial theme from server-side preferences
## ✅ Component Usage Patterns
### Correct: Using CSS Variables
```tsx
// ✅ GOOD: CSS Variables in className
<div className="bg-[var(--card)] text-[var(--foreground)] border-[var(--border)]">
// ✅ GOOD: CSS Variables in style prop
<div style={{ color: 'var(--primary)', backgroundColor: 'var(--card)' }}>
// ✅ GOOD: CSS Variables with color-mix for transparency
<div style={{
backgroundColor: 'color-mix(in srgb, var(--primary) 10%, transparent)',
borderColor: 'color-mix(in srgb, var(--primary) 20%, var(--border))'
}}>
```
### ❌ Forbidden: Tailwind Dark Mode Classes
```tsx
// ❌ BAD: Tailwind dark: classes
<div className="bg-white dark:bg-gray-800 text-black dark:text-white">
// ❌ BAD: Conditional classes
<div className={theme === 'dark' ? 'bg-gray-800' : 'bg-white'}>
// ❌ BAD: Hardcoded colors
<div className="bg-red-500 text-blue-600">
```
## ✅ Color System
### Semantic Color Tokens
- `--background`: Main background color
- `--foreground`: Main text color
- `--card`: Card/panel background
- `--card-hover`: Card hover state
- `--card-column`: Column background (darker than cards)
- `--border`: Border color
- `--input`: Input field background
- `--primary`: Primary brand color
- `--primary-foreground`: Text on primary background
- `--muted`: Muted text color
- `--muted-foreground`: Secondary text color
- `--accent`: Accent color (orange/amber)
- `--destructive`: Error/danger color (red)
- `--success`: Success color (green)
- `--purple`: Purple accent
- `--yellow`: Yellow accent
- `--green`: Green accent
- `--blue`: Blue accent
- `--gray`: Gray color
- `--gray-light`: Light gray background
### Color Mixing Patterns
```css
/* Background with transparency */
background-color: color-mix(in srgb, var(--primary) 10%, transparent);
/* Border with transparency */
border-color: color-mix(in srgb, var(--primary) 20%, var(--border));
/* Text with opacity */
color: color-mix(in srgb, var(--destructive) 80%, transparent);
```
## ✅ Theme Context Usage
### ThemeProvider Setup
```tsx
// In layout.tsx
<ThemeProvider initialTheme={initialPreferences.viewPreferences.theme}>
{children}
</ThemeProvider>
```
### Component Usage
```tsx
import { useTheme } from '@/contexts/ThemeContext';
function MyComponent() {
const { theme, toggleTheme, setTheme } = useTheme();
return (
<button onClick={toggleTheme}>
Switch to {theme === 'dark' ? 'light' : 'dark'} theme
</button>
);
}
```
## ✅ Future Extensibility
This system is designed to support:
- **Custom color themes**: Easy to add new color variables
- **User preferences**: Colors can be dynamically changed
- **Theme presets**: Multiple predefined themes
- **Accessibility**: High contrast modes
## 🚨 Anti-patterns to Avoid
1. **Don't mix approaches**: Never use both CSS variables and Tailwind dark: classes
2. **Don't duplicate theme application**: Theme should be applied only in ThemeContext
3. **Don't hardcode colors**: Always use semantic color tokens
4. **Don't use conditional classes**: Use CSS variables instead
5. **Don't forget transparency**: Use `color-mix()` for semi-transparent colors
## 📁 Key Files
- [globals.css](mdc:src/app/globals.css) - CSS Variables definitions
- [ThemeContext.tsx](mdc:src/contexts/ThemeContext.tsx) - Theme management
- [UserPreferencesContext.tsx](mdc:src/contexts/UserPreferencesContext.tsx) - Preferences sync
- [layout.tsx](mdc:src/app/layout.tsx) - Theme provider setup
Remember: **CSS Variables are the single source of truth for theming. Keep it pure and consistent.**

View File

@@ -5,26 +5,26 @@ alwaysApply: true
# Project Structure Rules
1. Backend:
- [services/](mdc:services/) - ALL database access
- [app/api/](mdc:app/api/) - API routes using services
- [src/services/](mdc:src/services/) - ALL database access
- [src/app/api/](mdc:src/app/api/) - API routes using services
2. Frontend:
- [clients/](mdc:clients/) - HTTP clients
- [components/](mdc:components/) - React components (organized by domain)
- [hooks/](mdc:hooks/) - React hooks
- [src/clients/](mdc:src/clients/) - HTTP clients
- [src/components/](mdc:src/components/) - React components (organized by domain)
- [src/hooks/](mdc:src/hooks/) - React hooks
3. Shared:
- [lib/](mdc:lib/) - Types and utilities
- [src/lib/](mdc:src/lib/) - Types and utilities
- [scripts/](mdc:scripts/) - Utility scripts
Key Files:
- [services/database.ts](mdc:services/database.ts) - Database pool
- [clients/base/http-client.ts](mdc:clients/base/http-client.ts) - Base HTTP client
- [lib/types.ts](mdc:lib/types.ts) - Shared types
- [src/services/database.ts](mdc:src/services/database.ts) - Database pool
- [src/clients/base/http-client.ts](mdc:src/clients/base/http-client.ts) - Base HTTP client
- [src/lib/types.ts](mdc:src/lib/types.ts) - Shared types
❌ FORBIDDEN:
- Database access outside services/
- HTTP calls outside clients/
- Business logic in components/
- Database access outside src/services/
- HTTP calls outside src/clients/
- Business logic in src/components/

View File

@@ -1,5 +1,5 @@
---
globs: services/*.ts
globs: src/services/*.ts
---
# Services Rules
@@ -7,7 +7,7 @@ globs: services/*.ts
1. Services MUST contain ALL PostgreSQL queries
2. Services are the ONLY layer allowed to communicate with the database
3. Each service MUST:
- Use the pool from [services/database.ts](mdc:services/database.ts)
- Use the pool from [src/services/database.ts](mdc:src/services/database.ts)
- Implement proper transaction management
- Handle errors and logging
- Validate data before insertion
@@ -37,6 +37,6 @@ export class MyService {
❌ FORBIDDEN:
- Direct database queries outside services
- Direct database queries outside src/services
- Raw SQL in API routes
- Database logic in components

3
.gitignore vendored
View File

@@ -43,4 +43,5 @@ next-env.d.ts
/src/generated/prisma
/prisma/dev.db
backups/
/data/*.db
/data/backups/*

32
.husky/post-commit Executable file
View File

@@ -0,0 +1,32 @@
#!/bin/sh
# Auto-version hook: incrémente la version après certains commits
# Récupérer le dernier message de commit
commit_msg=$(git log -1 --pretty=%B)
# Ignorer si le commit contient [skip version] ou si c'est un commit de version
if echo "$commit_msg" | grep -qE "\[skip version\]|chore: bump version"; then
exit 0
fi
# Vérifier si le commit devrait déclencher une mise à jour de version
# Types pris en charge:
# - feat: → minor bump
# - fix:, perf:, security:, patch:, refactor: → patch bump
# - feat!:, refactor!:, etc. (avec !) → major bump
# - breaking change ou BREAKING CHANGE → major bump
# Ignorés: chore:, docs:, style:, test:, ci:, build:
if echo "$commit_msg" | grep -qiE "^feat:|^fix:|^perf:|^security:|^patch:|^refactor:|^[a-z]+!:|breaking change"; then
# Lancer le script en mode hook (silent + ajout auto au staging)
pnpm tsx scripts/auto-version.ts --silent --hook
# Vérifier si package.json a changé
if ! git diff --quiet package.json; then
echo ""
echo "📦 Version mise à jour automatiquement"
echo "💡 Pour commit: git add package.json && git commit -m 'chore: bump version'"
fi
fi
exit 0

2
.husky/pre-commit Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/sh
lint-staged

3
.npmrc Normal file
View File

@@ -0,0 +1,3 @@
enable-pre-post-scripts=true
auto-install-peers=true

33
AGENTS.md Normal file
View File

@@ -0,0 +1,33 @@
# Repository Guidelines
## Project Structure & Responsibilities
Source lives in `src/`, with Next.js routes under `src/app` (UI) and API handlers in `src/app/api`—API files orchestrate services only. Domain logic, database queries, and external integrations reside in `src/services` (always use `database.ts`). HTTP clients belong in `src/clients`, React hooks in `src/hooks`, reusable UI in `src/components`, and shared utilities or types in `src/lib`. Operational helpers live in `scripts/`, Prisma schema and migrations in `prisma/`, and static assets in `public/`.
## Build, Test & Operational Commands
Start local development with `pnpm run dev` (Turbopack). Build production artifacts using `pnpm run build` and serve them via `pnpm run start`. Lint and type-check with `pnpm run lint`; run formatting verification through `pnpm run prettier:check` or fix issues via `pnpm run prettier:format`. Operational validations include `pnpm run backup:list`, `pnpm run backup:verify`, and cache utilities such as `pnpm run cache:stats`.
## Coding Style & Naming
Prettier and ESLint (`next/core-web-vitals`) enforce a 2-space, TypeScript-first style. Components and hooks use PascalCase, utilities use camelCase, and types live in `src/types` or `src/lib`. Co-locate component-specific assets near their implementation. Never bypass lint-staged; rely on Husky to run formatting before commits.
## Architecture & Data Flow Rules
Keep business logic out of the frontend: components, hooks, and clients may orchestrate UI state but must call services for domain decisions. Services are the only place for PostgreSQL queries and must expose typed interfaces with transaction handling. API routes validate input, call services, and return typed JSON—no raw SQL or business rules inline. Prefer Server Actions for straightforward mutations that require fast UI feedback; keep complex workflows, public endpoints, and integrations in API routes. Clients wrap HTTP calls, reuse the base HTTP client, and return typed responses.
## Styling & Theme System
All theming goes through CSS variables defined in `src/app/globals.css` and applied by `ThemeContext`. Do not use Tailwind `dark:` toggles or hard-coded colors; prefer `var(--token)` (and `color-mix` when translucency is needed). Keep semantic naming (`--card`, `--primary`, `--muted`) and extend the palette by adding variables instead of branching logic in components.
## Testing & Verification
Today's automated coverage relies on linting plus targeted scripts (e.g., `pnpm run test:story-points`, `pnpm run test:jira-fields`). When adding data flows or schedulers, contribute new headless scripts under `scripts/` and document manual QA steps in PRs. Name fixtures after the feature they back (`backlog-config.json`) and ensure linting passes before review.
## TODO Tracking & Workflow
If you complete an item in `TODO.md`, immediately flip its checkbox to checked without altering wording; add timestamps for major milestones. Mirror progress across sub-tasks and note blockers to keep the list trustworthy.
## Commit & PR Expectations
Follow the conventional prefixes visible in history (`feat:`, `refactor:`, `chore:`) with concise, action-oriented subjects. Group related changes per commit, documenting architectural context in the body when touching shared layers. Pull requests must summarize behavior changes, link issues, and attach screenshots for UI updates or schema diffs for Prisma changes. Confirm migrations, linting, formatting, and relevant scripts succeed before requesting review.

117
BACKUP.md
View File

@@ -40,16 +40,29 @@ TowerControl dispose d'un système de sauvegarde automatique et manuel complet p
```bash
# Voir la configuration actuelle
npm run backup:config
pnpm run backup:config
# Modifier la fréquence
tsx scripts/backup-manager.ts config-set interval=daily
pnpm tsx scripts/backup-manager.ts config-set interval=daily
# Modifier le nombre max de sauvegardes
tsx scripts/backup-manager.ts config-set maxBackups=10
pnpm tsx scripts/backup-manager.ts config-set maxBackups=10
# Activer/désactiver la compression
tsx scripts/backup-manager.ts config-set compression=true
pnpm tsx scripts/backup-manager.ts config-set compression=true
```
### Personnalisation du dossier de sauvegarde
```bash
# Via variable d'environnement permanente (.env)
BACKUP_STORAGE_PATH="./custom-backups"
# Via variable temporaire (une seule fois)
BACKUP_STORAGE_PATH="./my-backups" pnpm run backup:create
# Exemple avec un chemin absolu
BACKUP_STORAGE_PATH="/var/backups/towercontrol" pnpm run backup:create
```
## Utilisation
@@ -57,12 +70,14 @@ tsx scripts/backup-manager.ts config-set compression=true
### Interface graphique
#### Paramètres Avancés
- **Visualisation** du statut en temps réel
- **Création manuelle** de sauvegardes
- **Vérification** de l'intégrité
- **Lien** vers la gestion complète
#### Page de gestion complète
- **Configuration** détaillée du système
- **Liste** de toutes les sauvegardes
- **Actions** (supprimer, restaurer)
@@ -72,29 +87,29 @@ tsx scripts/backup-manager.ts config-set compression=true
```bash
# Créer une sauvegarde immédiate
npm run backup:create
pnpm run backup:create
# Lister toutes les sauvegardes
npm run backup:list
pnpm run backup:list
# Vérifier l'intégrité de la base
npm run backup:verify
pnpm run backup:verify
# Voir la configuration
npm run backup:config
pnpm run backup:config
# Démarrer le planificateur
npm run backup:start
pnpm run backup:start
# Arrêter le planificateur
npm run backup:stop
pnpm run backup:stop
# Statut du planificateur
npm run backup:status
pnpm run backup:status
# Commandes avancées (tsx requis)
tsx scripts/backup-manager.ts delete <filename>
tsx scripts/backup-manager.ts restore <filename> --force
# Commandes avancées (pnpm tsx requis)
pnpm tsx scripts/backup-manager.ts delete <filename>
pnpm tsx scripts/backup-manager.ts restore <filename> --force
```
## Planificateur automatique
@@ -115,13 +130,13 @@ En production, le planificateur démarre automatiquement 30 secondes après le l
```bash
# Démarrer manuellement
npm run backup:start
pnpm run backup:start
# Arrêter
npm run backup:stop
pnpm run backup:stop
# Voir le statut
npm run backup:status
pnpm run backup:status
```
## Fichiers de sauvegarde
@@ -140,6 +155,7 @@ Par défaut : `./backups/` (relatif au dossier du projet)
### Métadonnées
Chaque sauvegarde contient :
- **Horodatage** précis de création
- **Taille** du fichier
- **Type** (manuelle ou automatique)
@@ -159,17 +175,19 @@ Chaque sauvegarde contient :
### Procédure
#### Via interface (développement uniquement)
1. Aller dans la gestion des sauvegardes
2. Cliquer sur **"Restaurer"** à côté du fichier souhaité
3. Confirmer l'action
#### Via CLI
```bash
# Restaurer avec confirmation
tsx scripts/backup-manager.ts restore towercontrol_2025-01-15T10-30-00-000Z.db.gz
pnpm tsx scripts/backup-manager.ts restore towercontrol_2025-01-15T10-30-00-000Z.db.gz
# Restaurer en forçant (sans confirmation)
tsx scripts/backup-manager.ts restore towercontrol_2025-01-15T10-30-00-000Z.db.gz --force
pnpm tsx scripts/backup-manager.ts restore towercontrol_2025-01-15T10-30-00-000Z.db.gz --force
```
## Vérification d'intégrité
@@ -184,11 +202,11 @@ tsx scripts/backup-manager.ts restore towercontrol_2025-01-15T10-30-00-000Z.db.g
### Commandes
```bash
# Via npm script
npm run backup:verify
# Via pnpm script
pnpm run backup:verify
# Via CLI complet
tsx scripts/backup-manager.ts verify
pnpm tsx scripts/backup-manager.ts verify
```
### Vérifications effectuées
@@ -208,10 +226,10 @@ Le système supprime automatiquement les anciennes sauvegardes selon `maxBackups
```bash
# Supprimer une sauvegarde spécifique
tsx scripts/backup-manager.ts delete towercontrol_2025-01-15T10-30-00-000Z.db.gz
pnpm tsx scripts/backup-manager.ts delete towercontrol_2025-01-15T10-30-00-000Z.db.gz
# Forcer la suppression
tsx scripts/backup-manager.ts delete towercontrol_2025-01-15T10-30-00-000Z.db.gz --force
pnpm tsx scripts/backup-manager.ts delete towercontrol_2025-01-15T10-30-00-000Z.db.gz --force
```
### Surveillance des logs
@@ -223,6 +241,7 @@ Les opérations de sauvegarde sont loggées dans la console de l'application.
### Problèmes courants
#### Erreur "sqlite3 command not found"
```bash
# Sur macOS
brew install sqlite
@@ -232,6 +251,7 @@ sudo apt-get install sqlite3
```
#### Permissions insuffisantes
```bash
# Vérifier les permissions du dossier de sauvegarde
ls -la backups/
@@ -241,13 +261,14 @@ chmod 755 backups/
```
#### Espace disque insuffisant
```bash
# Vérifier l'espace disponible
df -h
# Supprimer d'anciennes sauvegardes
tsx scripts/backup-manager.ts list
tsx scripts/backup-manager.ts delete <filename>
pnpm tsx scripts/backup-manager.ts list
pnpm tsx scripts/backup-manager.ts delete <filename>
```
### Logs de debug
@@ -255,9 +276,11 @@ tsx scripts/backup-manager.ts delete <filename>
Pour activer le debug détaillé, modifier `services/database.ts` :
```typescript
export const prisma = globalThis.__prisma || new PrismaClient({
log: ['query', 'info', 'warn', 'error'], // Debug activé
});
export const prisma =
globalThis.__prisma ||
new PrismaClient({
log: ['query', 'info', 'warn', 'error'], // Debug activé
});
```
## Sécurité
@@ -272,8 +295,35 @@ export const prisma = globalThis.__prisma || new PrismaClient({
### Variables d'environnement
```bash
# Optionnel : personnaliser le chemin de la base
DATABASE_URL="file:./custom/path/dev.db"
# Configuration des chemins de base de données
DATABASE_URL="file:./prisma/dev.db" # Pour Prisma
BACKUP_DATABASE_PATH="./prisma/dev.db" # Base à sauvegarder (optionnel)
BACKUP_STORAGE_PATH="./backups" # Dossier des sauvegardes (optionnel)
```
### Docker
En environnement Docker, tout est centralisé dans le dossier `data/` :
```yaml
# docker-compose.yml
environment:
DATABASE_URL: 'file:./data/prod.db' # Base de données Prisma
BACKUP_DATABASE_PATH: './data/prod.db' # Base à sauvegarder
BACKUP_STORAGE_PATH: './data/backups' # Dossier des sauvegardes
volumes:
- ./data:/app/data # Bind mount vers dossier local
```
**Structure des dossiers :**
```
./data/ # Dossier local mappé
├── prod.db # Base de données production
├── dev.db # Base de données développement
└── backups/ # Sauvegardes (créé automatiquement)
├── towercontrol_*.db.gz
└── ...
```
## API
@@ -294,7 +344,7 @@ POST /api/backups/[filename] # Restaurer (dev seulement)
const response = await fetch('/api/backups', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'create' })
body: JSON.stringify({ action: 'create' }),
});
// Lister les sauvegardes
@@ -327,15 +377,16 @@ scripts/
## Roadmap
### Version actuelle ✅
- Sauvegardes automatiques et manuelles
- Interface graphique complète
- CLI d'administration
- Compression et rétention
### Améliorations futures 🚧
- Sauvegarde vers cloud (S3, Google Drive)
- Chiffrement des sauvegardes
- Notifications par email
- Métriques de performance
- Sauvegarde incrémentale

217
DOCKER.md Normal file
View File

@@ -0,0 +1,217 @@
# 🐳 Docker - TowerControl
Guide d'utilisation de TowerControl avec Docker.
## 🚀 Démarrage rapide
### Production
```bash
# Démarrer le service de production
docker-compose up -d towercontrol
# Accéder à l'application
open http://localhost:3006
```
### Développement
```bash
# Démarrer le service de développement avec live reload
docker-compose --profile dev up towercontrol-dev
# Accéder à l'application
open http://localhost:3005
```
## 📋 Services disponibles
### 🚀 `towercontrol` (Production)
- **Port** : 3006
- **Base de données** : `./data/prod.db`
- **Sauvegardes** : `./data/backups/`
- **Mode** : Optimisé, standalone
- **Restart** : Automatique
### 🛠️ `towercontrol-dev` (Développement)
- **Port** : 3005
- **Base de données** : `./data/dev.db`
- **Sauvegardes** : `./data/backups/` (partagées)
- **Mode** : Live reload, debug
- **Profile** : `dev`
## 📁 Structure des données
```
./data/ # Mappé vers /app/data dans les conteneurs
├── README.md # Documentation du dossier data
├── prod.db # Base SQLite production
├── dev.db # Base SQLite développement
└── backups/ # Sauvegardes automatiques
├── towercontrol_2025-01-15T10-30-00-000Z.db.gz
└── ...
```
## 🔧 Configuration
### Variables d'environnement
| Variable | Production | Développement | Description |
| ---------------------- | --------------------- | -------------------- | ---------------- |
| `NODE_ENV` | `production` | `development` | Mode d'exécution |
| `DATABASE_URL` | `file:./data/prod.db` | `file:./data/dev.db` | Base Prisma |
| `BACKUP_DATABASE_PATH` | `./data/prod.db` | `./data/dev.db` | Source backup |
| `BACKUP_STORAGE_PATH` | `./data/backups` | `./data/backups` | Dossier backup |
| `TZ` | `Europe/Paris` | `Europe/Paris` | Fuseau horaire |
### Ports
- **Production** : `3006:3000`
- **Développement** : `3005:3000`
## 📚 Commandes utiles
### Gestion des conteneurs
```bash
# Voir les logs
docker-compose logs -f towercontrol
docker-compose logs -f towercontrol-dev
# Arrêter les services
docker-compose down
# Reconstruire les images
docker-compose build
# Nettoyer tout
docker-compose down -v --rmi all
```
### Gestion des données
```bash
# Sauvegarder les données
docker-compose exec towercontrol pnpm run backup:create
# Lister les sauvegardes
docker-compose exec towercontrol pnpm run backup:list
# Accéder au shell du conteneur
docker-compose exec towercontrol sh
```
### Base de données
```bash
# Migrations Prisma
docker-compose exec towercontrol pnpm prisma migrate deploy
# Reset de la base (dev uniquement)
docker-compose exec towercontrol-dev pnpm prisma migrate reset
# Studio Prisma (dev)
docker-compose exec towercontrol-dev pnpm prisma studio
```
## 🔍 Debugging
### Vérifier la santé
```bash
# Health check
curl http://localhost:3006/api/health
curl http://localhost:3005/api/health
# Vérifier les variables d'env
docker-compose exec towercontrol env | grep -E "(DATABASE|BACKUP|NODE_ENV)"
```
### Logs détaillés
```bash
# Logs avec timestamps
docker-compose logs -f -t towercontrol
# Logs des 100 dernières lignes
docker-compose logs --tail=100 towercontrol
```
## 🚨 Dépannage
### Problèmes courants
**Port déjà utilisé**
```bash
# Trouver le processus qui utilise le port
lsof -i :3006
kill -9 <PID>
```
**Base de données corrompue**
```bash
# Restaurer depuis une sauvegarde
docker-compose exec towercontrol pnpm run backup:restore filename.db.gz
```
**Permissions**
```bash
# Corriger les permissions du dossier data
sudo chown -R $USER:$USER ./data
```
## 📊 Monitoring
### Espace disque
```bash
# Taille du dossier data
du -sh ./data
# Espace libre
df -h .
```
### Performance
```bash
# Stats des conteneurs
docker stats
# Utilisation mémoire
docker-compose exec towercontrol free -h
```
## 🔒 Production
### Recommandations
- Utiliser un reverse proxy (nginx, traefik)
- Configurer HTTPS
- Sauvegarder régulièrement `./data/`
- Monitorer l'espace disque
- Logs centralisés
### Exemple nginx
```nginx
server {
listen 80;
server_name towercontrol.example.com;
location / {
proxy_pass http://localhost:3006;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
---
📚 **Voir aussi** : [data/README.md](./data/README.md)

View File

@@ -1,6 +1,11 @@
# Multi-stage Dockerfile for Next.js with Prisma
FROM node:20-alpine AS base
# Install pnpm
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
@@ -8,9 +13,13 @@ RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json package-lock.json* ./
COPY package.json pnpm-lock.yaml* ./
# Copy Prisma schema for postinstall script
COPY prisma ./prisma
# Set dummy DATABASE_URL for Prisma client generation during postinstall
ENV DATABASE_URL="file:/tmp/build.db"
RUN \
if [ -f package-lock.json ]; then npm install; \
if [ -f pnpm-lock.yaml ]; then pnpm install --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
@@ -23,20 +32,17 @@ COPY . .
# Set a dummy DATABASE_URL for build time (Prisma needs it to generate client)
ENV DATABASE_URL="file:/tmp/build.db"
# Generate Prisma client
RUN npx prisma generate
# Initialize the database schema for build time
RUN npx prisma migrate deploy || npx prisma db push
# Generate Prisma client (no DB needed at build time)
RUN pnpm prisma generate
# Build the application
RUN npm run build
RUN pnpm run build
# Production image, copy all the files and run next
FROM base AS runner
# Set timezone to Europe/Paris
RUN apk add --no-cache tzdata
# Set timezone to Europe/Paris and install sqlite3 for backups
RUN apk add --no-cache tzdata sqlite su-exec
RUN ln -snf /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo Europe/Paris > /etc/timezone
WORKDIR /app
@@ -60,21 +66,25 @@ RUN chown nextjs:nodejs .next
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# Copy Prisma files
# Copy Prisma schema and migrations
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
# Create data directory for SQLite
RUN mkdir -p /app/data && chown nextjs:nodejs /app/data
# Copy pnpm node_modules (includes .pnpm store with Prisma client)
COPY --from=builder /app/node_modules ./node_modules
# Create data directory for SQLite and backups (will be overridden by volume mount but ensures it exists)
RUN mkdir -p /app/data/backups && chmod -R 777 /app/data
# Set all ENV vars before switching user
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
ENV TZ=Europe/Paris
USER nextjs
EXPOSE 3000
# Start the application with database migration
CMD ["sh", "-c", "npx prisma migrate deploy && node server.js"]
# Start the application with Prisma migrations
# Fix permissions for data directory (volume mount may have wrong ownership)
# Then switch to nextjs user and run migrations
# For fresh DBs: use db push to apply schema, then mark migrations as applied
# For existing DBs: use migrate deploy to apply incremental migrations
CMD ["sh", "-c", "mkdir -p /app/data/backups && chown -R nextjs:nodejs /app || chmod -R 755 /app || true; chmod -R 777 /app/data && chown -R nextjs:nodejs /app/data || true; exec su-exec nextjs sh -c 'set +e; if ! pnpm prisma migrate deploy; then echo \"Migration failed, using db push for fresh database...\"; pnpm prisma db push --accept-data-loss --skip-generate; for migration in prisma/migrations/*/; do if [ -d \"$migration\" ] && [ -f \"$migration/migration.sql\" ]; then migration_name=$(basename \"$migration\"); pnpm prisma migrate resolve --applied \"$migration_name\" 2>/dev/null || true; fi; done; fi; set -e; exec node server.js'"]

157
PNPM_MIGRATION.md Normal file
View File

@@ -0,0 +1,157 @@
# Migration vers pnpm
## ✅ Changements effectués
### 1. Nettoyage npm
- ✅ Suppression de `node_modules/`
- ✅ Suppression de `package-lock.json`
### 2. Installation pnpm
- ✅ Installation des dépendances avec `pnpm install`
- ✅ Création de `pnpm-lock.yaml`
- ✅ Ajout de `tailwindcss` comme dépendance dev explicite (requis par pnpm)
### 3. Configuration
- ✅ Création de `.npmrc` avec :
- `enable-pre-post-scripts=true`
- `auto-install-peers=true`
- ✅ Rebuild des packages critiques : `@prisma/client`, `prisma`, `esbuild`, `sharp`, `@tailwindcss/oxide`
### 4. Scripts package.json
- ✅ Remplacement de tous les `npx` par `pnpm` dans les scripts
- Scripts modifiés :
- `backup:*` (create, list, verify, config, start, stop, status)
- `cache:*` (monitor, stats, cleanup, clear)
- `test:*` (story-points, jira-fields)
### 5. Dockerfile
- ✅ Installation de pnpm via `corepack enable`
- ✅ Variables d'environnement `PNPM_HOME` et `PATH`
- ✅ Remplacement de `package-lock.json` par `pnpm-lock.yaml`
- ✅ Remplacement de `npm install` par `pnpm install --frozen-lockfile`
- ✅ Remplacement de tous les `npx`/`npm` par `pnpm`
### 6. docker-compose.yml
- ✅ Mise à jour du service `towercontrol-dev` pour utiliser pnpm
### 7. Documentation
- ✅ Mise à jour de `README.md` :
- Prérequis : `pnpm 9+` au lieu de `npm` ou `yarn`
- Toutes les commandes d'installation et d'utilisation
- Scripts disponibles
## 🧪 Tests effectués
-`pnpm install` - Installation réussie
-`pnpm prisma generate` - Génération du client Prisma OK
-`pnpm run lint` - Linting réussi
-`pnpm run build` - Build de production réussi
## 📦 Nouvelles dépendances ajoutées
- `tailwindcss` (devDependencies) - Requis explicitement pour l'import CSS avec pnpm
## ⚠️ Points d'attention
### Warning workspace root
Un warning apparaît lors du build :
```
Warning: Next.js inferred your workspace root, but it may not be correct.
We detected multiple lockfiles and selected the directory of /Users/julien.froidefond/package-lock.json
```
**Cause** : Un `package-lock.json` existe dans `/Users/julien.froidefond/`
**Solutions** :
1. Supprimer le lockfile parent si inutilisé
2. Ou ajouter dans `next.config.ts` :
```typescript
turbopack: {
root: process.cwd(),
}
```
### Peer dependency warning
```
@emoji-mart/react 1.1.1 requires peer react@"^16.8 || ^17 || ^18" but found 19.1.0
```
**Impact** : Aucun pour le moment, le projet fonctionne avec React 19
**Action** : À surveiller lors des mises à jour de `@emoji-mart/react`
## 🚀 Commandes usuelles
### Développement
```bash
pnpm install # Installer les dépendances
pnpm run dev # Mode développement
pnpm run build # Build de production
pnpm run start # Démarrer en production
```
### Base de données
```bash
pnpm prisma studio # Interface graphique
pnpm prisma generate # Regénérer le client
pnpm prisma db push # Appliquer le schéma
```
### Qualité
```bash
pnpm run lint # ESLint
pnpm run prettier:format # Formatter
pnpm run prettier:check # Vérifier le formatage
```
### Docker
```bash
docker compose up -d # Production (port 3006)
docker compose --profile dev up -d # Développement (port 3005)
docker compose down # Arrêter
docker compose build --no-cache # Rebuild complet
```
## 📝 Avantages de pnpm
1. **Performance** : Installation plus rapide (liens symboliques)
2. **Espace disque** : Économie grâce au store global
3. **Sécurité** : Structure node_modules stricte (pas d'accès aux dépendances non déclarées)
4. **Monorepo** : Support natif des workspaces
5. **Déterminisme** : Lockfile plus fiable
## 🔄 Rollback vers npm
Si besoin de revenir à npm :
```bash
# Supprimer pnpm
rm -rf node_modules pnpm-lock.yaml .npmrc
# Restaurer les anciens scripts dans package.json
# Restaurer l'ancien Dockerfile
# Restaurer l'ancien docker-compose.yml
# Réinstaller avec npm
npm install --legacy-peer-deps
```
---
**Date de migration** : 16 octobre 2025
**Version pnpm** : 10.15.1
**Version Node** : 20

270
README.md
View File

@@ -20,6 +20,7 @@ TowerControl est un gestionnaire de tâches **standalone** conçu pour les déve
## ✨ Fonctionnalités principales
### 🏗️ Kanban moderne
- **Drag & drop fluide** avec @dnd-kit (optimistic updates)
- **Colonnes configurables** : backlog, todo, in_progress, done, cancelled, freeze, archived
- **Vues multiples** : Kanban classique + swimlanes par priorité
@@ -27,18 +28,21 @@ TowerControl est un gestionnaire de tâches **standalone** conçu pour les déve
- **Création rapide** : Ajout inline dans chaque colonne
### 🏷️ Système de tags avancé
- **Tags colorés** avec sélecteur de couleur
- **Autocomplete intelligent** lors de la saisie
- **Filtrage en temps réel** par tags
- **Gestion complète** avec page dédiée `/tags`
### 📊 Filtrage et recherche
- **Recherche temps réel** dans les titres et descriptions
- **Filtres combinables** : statut, priorité, tags, source
- **Tri flexible** : date, priorité, alphabétique
- **Interface intuitive** avec dropdowns et toggles
### 📝 Daily Notes
- **Checkboxes quotidiennes** avec sections "Hier" / "Aujourd'hui"
- **Navigation par date** (précédent/suivant)
- **Liaison optionnelle** avec les tâches existantes
@@ -46,6 +50,7 @@ TowerControl est un gestionnaire de tâches **standalone** conçu pour les déve
- **Historique calendaire** des dailies
### 🔗 Intégration Jira Cloud
- **Synchronisation unidirectionnelle** (Jira → local)
- **Authentification sécurisée** (email + API token)
- **Mapping intelligent** des statuts Jira
@@ -54,6 +59,7 @@ TowerControl est un gestionnaire de tâches **standalone** conçu pour les déve
- **Interface de configuration** complète
### 🎨 Interface & UX
- **Thème adaptatif** : dark/light + détection système
- **Design cohérent** : palette cyberpunk/tech avec Tailwind CSS
- **Composants modulaires** : Button, Input, Card, Modal, Badge
@@ -61,6 +67,7 @@ TowerControl est un gestionnaire de tâches **standalone** conçu pour les déve
- **Responsive design** pour tous les écrans
### ⚡ Performance & Architecture
- **Server Actions** pour les mutations rapides (vs API routes)
- **Architecture SSR** avec hydratation optimisée
- **Base de données SQLite** ultra-rapide
@@ -72,8 +79,9 @@ TowerControl est un gestionnaire de tâches **standalone** conçu pour les déve
## 🛠️ Installation
### Prérequis
- **Node.js** 18+
- **npm** ou **yarn**
- **pnpm** 9+
### Installation locale
@@ -83,17 +91,17 @@ git clone https://github.com/votre-repo/towercontrol.git
cd towercontrol
# Installer les dépendances
npm install
pnpm install
# Configurer la base de données
npx prisma generate
npx prisma db push
pnpm prisma generate
pnpm prisma db push
# (Optionnel) Ajouter des données de test
npm run seed
pnpm run seed
# Démarrer en développement
npm run dev
pnpm run dev
```
L'application sera accessible sur **http://localhost:3000**
@@ -115,10 +123,12 @@ docker compose --profile dev up -d
```
**Accès :**
- **Production** : http://localhost:3006
- **Développement** : http://localhost:3005
**Gestion des données :**
```bash
# Utiliser votre base locale existante (décommentez dans docker-compose.yml)
# - ./prisma/dev.db:/app/data/prod.db
@@ -134,6 +144,7 @@ docker compose down -v
```
**Avantages Docker :**
-**Isolation complète** - Pas de pollution de l'environnement local
-**Base persistante** - Volumes Docker pour SQLite
-**Prêt pour prod** - Configuration optimisée
@@ -182,31 +193,204 @@ JIRA_API_TOKEN="votre_token_api"
```
towercontrol/
├── src/
│ ├── app/ # Pages Next.js 15 (App Router)
│ ├── app/ # Next.js 15 App Router (pages & routes)
│ │ ├── api/ # API Routes (endpoints complexes)
│ │ ├── daily/ # Page daily notes
│ │ ├── tags/ # Page gestion tags
│ │ └── settings/ # Page configuration
│ │ │ ├── analytics/ # Endpoints d'analytics
│ │ │ ├── auth/ # Authentification (NextAuth)
│ │ │ ├── backups/ # Gestion des sauvegardes
│ │ │ ├── daily/ # API daily notes
│ │ │ ├── jira/ # API intégration Jira
│ │ │ ├── notes/ # API notes markdown
│ │ │ ├── tags/ # API gestion tags
│ │ │ ├── tasks/ # API tâches
│ │ │ ├── tfs/ # API intégration TFS
│ │ │ └── user-preferences/ # API préférences utilisateur
│ │ ├── daily/ # Page daily notes (/daily)
│ │ ├── jira-dashboard/ # Dashboard Jira (/jira-dashboard)
│ │ ├── kanban/ # Page Kanban (/kanban)
│ │ ├── manager/ # Page manager
│ │ ├── notes/ # Page notes (/notes)
│ │ ├── profile/ # Page profil utilisateur
│ │ ├── settings/ # Page configuration (/settings)
│ │ │ ├── advanced/ # Paramètres avancés
│ │ │ ├── backup/ # Gestion backups
│ │ │ ├── general/ # Paramètres généraux
│ │ │ └── integrations/ # Config intégrations
│ │ ├── weekly-manager/ # Page weekly manager
│ │ ├── layout.tsx # Layout principal
│ │ ├── page.tsx # Page d'accueil (/)
│ │ └── globals.css # Styles globaux + variables CSS
│ │
│ ├── actions/ # Server Actions (mutations rapides)
└── contexts/ # Contexts React globaux
├── components/
│ ├── ui/ # Composants UI de base
│ ├── kanban/ # Composants Kanban
│ ├── daily/ # Composants Daily notes
└── forms/ # Formulaires réutilisables
├── services/ # Services backend (logique métier)
├── database.ts # Pool Prisma
│ ├── tasks.ts # CRUD tâches
│ ├── tags.ts # CRUD tags
├── daily.ts # Daily notes
├── jira.ts # Intégration Jira
└── user-preferences.ts # Préférences utilisateur
├── clients/ # Clients HTTP frontend
├── hooks/ # Hooks React personnalisés
├── lib/ # Utilitaires et types
└── prisma/ # Schéma et migrations DB
│ ├── backup.ts # Actions sauvegardes
│ │ ├── daily.ts # Actions daily notes
│ ├── jira-analytics.ts # Actions analytics Jira
│ ├── preferences.ts # Actions préférences
│ ├── tags.ts # Actions tags
│ ├── tasks.ts # Actions tâches
└── tfs.ts # Actions TFS
│ ├── components/ # Composants React (UI uniquement)
│ ├── ui/ # Composants UI de base réutilisables
├── Button.tsx # Boutons
├── Input.tsx # Inputs
│ │ ├── Modal.tsx # Modales
├── Badge.tsx # Badges
├── Card.tsx # Cartes
└── ... # Autres composants UI
├── kanban/ # Composants Kanban spécifiques
│ │ │ ├── Board.tsx # Board principal
│ │ │ ├── Column.tsx # Colonne Kanban
│ │ │ ├── TaskCard.tsx # Carte de tâche
│ │ │ ├── filters/ # Composants de filtrage
│ │ │ └── ... # Autres composants Kanban
│ │ ├── daily/ # Composants Daily notes
│ │ ├── dashboard/ # Composants dashboard
│ │ ├── forms/ # Formulaires réutilisables
│ │ ├── jira/ # Composants intégration Jira
│ │ ├── settings/ # Composants paramètres
│ │ └── charts/ # Composants graphiques
│ │
│ ├── services/ # Services backend (logique métier)
│ │ ├── core/ # Services core
│ │ │ ├── database.ts # Pool Prisma (unique point d'accès DB)
│ │ │ ├── system-info.ts # Infos système
│ │ │ └── user-preferences.ts # Préférences utilisateur
│ │ ├── task-management/ # Gestion des tâches
│ │ │ ├── tasks.ts # CRUD tâches
│ │ │ ├── tags.ts # CRUD tags
│ │ │ └── daily.ts # Daily notes
│ │ ├── integrations/ # Intégrations externes
│ │ │ ├── jira/ # Intégration Jira
│ │ │ │ ├── jira.ts # Client Jira API
│ │ │ │ ├── analytics.ts # Analytics Jira
│ │ │ │ ├── scheduler.ts # Planification sync
│ │ │ │ └── ... # Autres services Jira
│ │ │ └── tfs/ # Intégration TFS
│ │ ├── analytics/ # Services d'analytics
│ │ │ ├── analytics.ts # Analytics générales
│ │ │ ├── metrics.ts # Métriques
│ │ │ └── ... # Autres analytics
│ │ └── data-management/ # Gestion des données
│ │ ├── backup.ts # Sauvegardes
│ │ └── backup-scheduler.ts # Planification backups
│ │
│ ├── clients/ # Clients HTTP frontend
│ │ ├── base/ # Client HTTP de base
│ │ │ └── http-client.ts # Client HTTP réutilisable
│ │ ├── tasks-client.ts # Client API tâches
│ │ ├── tags-client.ts # Client API tags
│ │ ├── daily-client.ts # Client API daily
│ │ ├── jira-client.ts # Client API Jira
│ │ └── backup-client.ts # Client API backups
│ │
│ ├── hooks/ # Hooks React personnalisés
│ │ ├── useTasks.ts # Hook gestion tâches
│ │ ├── useTags.ts # Hook gestion tags
│ │ ├── useDaily.ts # Hook daily notes
│ │ ├── useDragAndDrop.ts # Hook drag & drop
│ │ └── ... # Autres hooks
│ │
│ ├── contexts/ # Contexts React globaux
│ │ ├── ThemeContext.tsx # Gestion thème dark/light
│ │ ├── TasksContext.tsx # Context tâches
│ │ ├── UserPreferencesContext.tsx # Préférences utilisateur
│ │ └── ... # Autres contexts
│ │
│ ├── lib/ # Utilitaires et configuration
│ │ ├── types.ts # Types TypeScript partagés
│ │ ├── utils.ts # Fonctions utilitaires
│ │ ├── config.ts # Configuration app
│ │ ├── status-config.ts # Configuration statuts Kanban
│ │ ├── tag-colors.ts # Configuration couleurs tags
│ │ └── ... # Autres utilitaires
│ │
│ ├── types/ # Types TypeScript spécifiques
│ │ └── next-auth.d.ts # Types NextAuth
│ │
│ └── middleware.ts # Middleware Next.js (auth, etc.)
├── prisma/ # Prisma ORM
│ ├── schema.prisma # Schéma de base de données
│ └── migrations/ # Migrations SQL
├── scripts/ # Scripts utilitaires
│ ├── backup-manager.ts # Gestion backups
│ ├── seed-data.ts # Données de test
│ └── ... # Autres scripts
├── public/ # Assets statiques
│ └── icons/ # Icônes
├── data/ # Données locales
│ ├── dev.db # Base SQLite développement
│ ├── prod.db # Base SQLite production
│ └── backups/ # Sauvegardes automatiques
└── [fichiers racine] # Config projet (package.json, etc.)
```
### Explication détaillée des dossiers
#### 📁 `src/app/` - Pages et routes Next.js
- **Pages publiques** : Routes Next.js qui génèrent les pages (`page.tsx`)
- **API Routes** : Endpoints HTTP dans `/api` pour les opérations complexes
- **Client Components** : Composants client séparés (`*PageClient.tsx`) pour l'hydratation
- **Layout** : Layout global avec providers (Theme, Auth, etc.)
#### 📁 `src/actions/` - Server Actions
- **Mutations rapides** : Actions serveur pour les mutations simples (CRUD)
- **Cache intelligent** : Révalidation automatique avec `revalidatePath()`
- **UX optimisée** : Utilisation avec `useTransition` pour les états de chargement
#### 📁 `src/components/` - Composants React (UI uniquement)
- **Règle stricte** : AUCUNE logique métier, uniquement présentation
- **Organisation par domaine** : `kanban/`, `daily/`, `jira/`, etc.
- **Composants UI réutilisables** : Dans `ui/` pour la cohérence visuelle
- **Formulaires** : Dans `forms/` pour la réutilisation
#### 📁 `src/services/` - Logique métier backend
- **Point unique d'accès DB** : `core/database.ts` (Pool Prisma)
- **Séparation par domaine** : `task-management/`, `integrations/`, `analytics/`
- **Règle stricte** : TOUTE la logique métier ici, jamais dans les composants
- **Services métier** : CRUD, calculs, validations, intégrations externes
#### 📁 `src/clients/` - Clients HTTP frontend
- **Client HTTP de base** : `base/http-client.ts` avec gestion erreurs/tokens
- **Clients par domaine** : Un client par API (tasks, tags, jira, etc.)
- **Règle stricte** : Uniquement requêtes HTTP, pas de logique métier
#### 📁 `src/hooks/` - Hooks React personnalisés
- **Orchestration UI** : Gestion état React, appels API via clients
- **Logique UI uniquement** : Pas de logique métier, uniquement coordination
#### 📁 `src/contexts/` - Contexts React globaux
- **État global** : Thème, préférences, tâches, etc.
- **Providers** : Utilisés dans le layout principal
#### 📁 `src/lib/` - Utilitaires et configuration
- **Types partagés** : `types.ts` pour la cohérence TypeScript
- **Configurations** : Statuts Kanban, couleurs tags, etc.
- **Helpers** : Fonctions utilitaires (dates, formatting, etc.)
#### 📁 `prisma/` - Base de données
- **Schéma** : Définition des modèles (`schema.prisma`)
- **Migrations** : Historique des changements de schéma
#### 📁 `scripts/` - Scripts utilitaires
- **Opérations** : Backups, seeding, maintenance
- **Exécution** : Via `pnpm run <script-name>`
### Stack technique
- **Frontend** : Next.js 15, React 19, TypeScript, Tailwind CSS
@@ -262,22 +446,22 @@ towercontrol/
```bash
# Développement
npm run dev # Démarrer en mode dev avec Turbopack
npm run build # Build de production
npm run start # Démarrer en production
pnpm run dev # Démarrer en mode dev avec Turbopack
pnpm run build # Build de production
pnpm run start # Démarrer en production
# Base de données
npx prisma studio # Interface graphique BDD
npx prisma generate # Regénérer le client Prisma
npx prisma db push # Appliquer le schema à la BDD
npx prisma migrate dev # Créer une migration
pnpm prisma studio # Interface graphique BDD
pnpm prisma generate # Regénérer le client Prisma
pnpm prisma db push # Appliquer le schema à la BDD
pnpm prisma migrate dev # Créer une migration
# Qualité de code
npm run lint # ESLint + Prettier
npx tsc --noEmit # Vérification TypeScript
pnpm run lint # ESLint + Prettier
pnpm tsc --noEmit # Vérification TypeScript
# Scripts utilitaires
npm run seed # Ajouter des données de test
pnpm run seed # Ajouter des données de test
```
---
@@ -289,10 +473,10 @@ npm run seed # Ajouter des données de test
```typescript
// lib/config.ts
export const UI_CONFIG = {
theme: 'system', // 'light' | 'dark' | 'system'
itemsPerPage: 50, // Pagination
enableDragAndDrop: true, // Drag & drop
autoSave: true // Sauvegarde auto
theme: 'system', // 'light' | 'dark' | 'system'
itemsPerPage: 50, // Pagination
enableDragAndDrop: true, // Drag & drop
autoSave: true, // Sauvegarde auto
};
```
@@ -322,6 +506,7 @@ DATABASE_URL="postgresql://user:pass@localhost:5432/towercontrol"
## 🚧 Roadmap
### ✅ Version 2.0 (Actuelle)
- Interface Kanban moderne avec drag & drop
- Système de tags avancé
- Daily notes avec navigation
@@ -330,12 +515,14 @@ DATABASE_URL="postgresql://user:pass@localhost:5432/towercontrol"
- Server Actions pour les performances
### 🔄 Version 2.1 (En cours)
- [ ] Page dashboard avec analytics
- [ ] Système de sauvegarde automatique (configurable)
- [ ] Métriques de productivité et graphiques
- [ ] Actions en lot (sélection multiple)
### 🎯 Version 2.2 (Futur)
- [ ] Sous-tâches et hiérarchie
- [ ] Dates d'échéance et rappels
- [ ] Collaboration et assignation
@@ -343,6 +530,7 @@ DATABASE_URL="postgresql://user:pass@localhost:5432/towercontrol"
- [ ] Mode PWA et offline
### 🚀 Version 3.0 (Vision)
- [ ] Analytics d'équipe avancées
- [ ] Intégrations multiples (GitHub, Linear, etc.)
- [ ] API publique et webhooks

116
TFS_UPGRADE_SUMMARY.md Normal file
View File

@@ -0,0 +1,116 @@
# Mise à niveau TFS : Récupération des PRs assignées à l'utilisateur
## 🎯 Objectif
Permettre au service TFS de récupérer **toutes** les Pull Requests assignées à l'utilisateur sur l'ensemble de son organisation Azure DevOps, plutôt que de se limiter à un projet spécifique.
## ⚡ Changements apportés
### 1. Service TFS (`src/services/tfs.ts`)
#### Nouvelles méthodes ajoutées :
- **`getMyPullRequests()`** : Récupère toutes les PRs concernant l'utilisateur
- **`getPullRequestsByCreator()`** : PRs créées par l'utilisateur
- **`getPullRequestsByReviewer()`** : PRs où l'utilisateur est reviewer
- **`filterPullRequests()`** : Applique les filtres de configuration
#### Méthode syncTasks refactorisée :
- Utilise maintenant `getMyPullRequests()` au lieu de parcourir tous les repositories
- Plus efficace et centrée sur l'utilisateur
- Récupération directe via l'API Azure DevOps avec critères `@me`
#### Configuration mise à jour :
- **`projectName`** devient **optionnel**
- Validation assouplie dans les factories
- Comportement adaptatif : projet spécifique OU toute l'organisation
### 2. Interface utilisateur (`src/components/settings/TfsConfigForm.tsx`)
#### Modifications du formulaire :
- Champ "Nom du projet" marqué comme **optionnel**
- Validation `required` supprimée
- Placeholder mis à jour : _"laisser vide pour toute l'organisation"_
- Affichage du statut : _"Toute l'organisation"_ si pas de projet
#### Instructions mises à jour :
- Explique le nouveau comportement **synchronisation intelligente**
- Précise que les PRs sont récupérées automatiquement selon l'assignation
- Note sur la portée projet vs organisation
### 3. Endpoints API
#### `/api/tfs/test/route.ts`
- Validation mise à jour (projectName optionnel)
- Message de réponse enrichi avec portée (projet/organisation)
- Retour détaillé du scope de synchronisation
#### `/api/tfs/sync/route.ts`
- Validation assouplie pour les deux méthodes GET/POST
- Configuration adaptative selon la présence du projectName
## 🔧 API Azure DevOps utilisées
### Nouvelles requêtes :
```typescript
// PRs créées par l'utilisateur
/_apis/git/pullrequests?searchCriteria.creatorId=@me&searchCriteria.status=active
// PRs où je suis reviewer
/_apis/git/pullrequests?searchCriteria.reviewerId=@me&searchCriteria.status=active
```
### Comportement intelligent :
- **Fusion automatique** des deux types de PRs
- **Déduplication** basée sur `pullRequestId`
- **Filtrage** selon la configuration (repositories, branches, projet)
## 📊 Avantages
1. **Centré utilisateur** : Récupère seulement les PRs pertinentes
2. **Performance améliorée** : Une seule requête API au lieu de parcourir tous les repos
3. **Flexibilité** : Projet spécifique OU toute l'organisation
4. **Scalabilité** : Fonctionne avec des organisations de grande taille
5. **Simplicité** : Configuration minimale requise
## 🎨 Interface utilisateur
### Avant :
- Champ projet **obligatoire**
- Synchronisation limitée à UN projet
- Configuration rigide
### Après :
- Champ projet **optionnel**
- Synchronisation intelligente de TOUTES les PRs assignées
- Configuration flexible et adaptative
- Instructions claires sur le comportement
## ✅ Tests recommandés
1. **Configuration avec projet spécifique** : Vérifier le filtrage par projet
2. **Configuration sans projet** : Vérifier la récupération organisation complète
3. **Test de connexion** : Valider le nouveau comportement API
4. **Synchronisation** : Contrôler que seules les PRs assignées sont récupérées
## 🚀 Déploiement
La migration est **transparente** :
- Les configurations existantes continuent à fonctionner
- Possibilité de supprimer le `projectName` pour étendre la portée
- Pas de rupture de compatibilité
---
_Cette mise à niveau transforme le service TFS d'un outil de surveillance de projet en un assistant personnel intelligent pour Azure DevOps._ 🎯

661
TODO.md
View File

@@ -1,394 +1,285 @@
# TowerControl v2.0 - Gestionnaire de tâches moderne
## ✅ Phase 1: Nettoyage et architecture (TERMINÉ)
## Fix
### 1.1 Configuration projet Next.js
- [x] Initialiser Next.js avec TypeScript
- [x] Configurer ESLint, Prettier
- [x] Setup structure de dossiers selon les règles du workspace
- [x] Configurer base de données (SQLite local)
- [x] Setup Prisma ORM
- [ ] Calendrier n'a plus le bouton calendrier d'ouverture du calendrier visuel dans les inputs datetime
- [ ] Un raccourci pour chercher dans la page de Kanban
- [ ] Bouton cloner une tache dans la modale d'edition
### 1.2 Architecture backend standalone
- [x] Créer `services/database.ts` - Pool de connexion DB
- [x] Créer `services/tasks.ts` - Service CRUD pour les tâches
- [x] Créer `lib/types.ts` - Types partagés (Task, Tag, etc.)
- [x] Nettoyer l'ancien code de synchronisation
## Idées à developper
### 1.3 API moderne et propre
- [x] `app/api/tasks/route.ts` - API CRUD complète (GET, POST, PATCH, DELETE)
- [x] Supprimer les routes de synchronisation obsolètes
- [x] Configuration moderne dans `lib/config.ts`
**Architecture finale** : App standalone avec backend propre et API REST moderne
## 🎯 Phase 2: Interface utilisateur moderne (EN COURS)
### 2.1 Système de design et composants UI
- [x] Créer les composants UI de base (Button, Input, Card, Modal, Badge)
- [x] Implémenter le système de design tech dark (couleurs, typographie, spacing)
- [x] Setup Tailwind CSS avec classes utilitaires personnalisées
- [x] Créer une palette de couleurs tech/cyberpunk
### 2.2 Composants Kanban existants (à améliorer)
- [x] `components/kanban/Board.tsx` - Tableau Kanban principal
- [x] `components/kanban/Column.tsx` - Colonnes du Kanban
- [x] `components/kanban/TaskCard.tsx` - Cartes de tâches
- [x] `components/ui/Header.tsx` - Header avec statistiques
- [x] Refactoriser les composants pour utiliser le nouveau système UI
### 2.3 Gestion des tâches (CRUD)
- [x] Formulaire de création de tâche (Modal + Form)
- [x] Création rapide inline dans les colonnes (QuickAddTask)
- [x] Formulaire d'édition de tâche (Modal + Form avec pré-remplissage)
- [x] Édition inline du titre des tâches (clic sur titre → input)
- [x] Suppression de tâche (icône discrète + API call)
- [x] Changement de statut par drag & drop (@dnd-kit)
- [x] Validation des formulaires et gestion d'erreurs
### 2.4 Gestion des tags
- [x] Créer/éditer des tags avec sélecteur de couleur
- [x] Autocomplete pour les tags existants
- [x] Suppression de tags (avec vérification des dépendances)
- [x] Affichage des tags avec couleurs personnalisées
- [x] Service tags avec CRUD complet (Prisma)
- [x] API routes /api/tags avec validation
- [x] Client HTTP et hook useTags
- [x] Composants UI (TagInput, TagDisplay, TagForm)
- [x] Intégration dans les formulaires (TagInput avec autocomplete)
- [x] Intégration dans les TaskCards (TagDisplay avec couleurs)
- [x] Contexte global pour partager les tags
- [x] Page de gestion des tags (/tags) avec interface complète
- [x] Navigation dans le Header (Kanban ↔ Tags)
- [x] Filtrage par tags (intégration dans Kanban)
- [x] Interface de filtrage complète (recherche, priorités, tags)
- [x] Logique de filtrage temps réel dans le contexte
- [x] Intégration des filtres dans KanbanBoard
### 2.5 Clients HTTP et hooks
- [x] `clients/tasks-client.ts` - Client pour les tâches (CRUD complet)
- [x] `clients/tags-client.ts` - Client pour les tags
- [x] `clients/base/http-client.ts` - Client HTTP de base
- [x] `hooks/useTasks.ts` - Hook pour la gestion des tâches (CRUD complet)
- [x] `hooks/useTags.ts` - Hook pour la gestion des tags
- [x] Drag & drop avec @dnd-kit (intégré directement dans Board.tsx)
- [x] Gestion des erreurs et loading states
- [x] Architecture SSR + hydratation client optimisée
### 2.6 Fonctionnalités Kanban avancées
- [x] Drag & drop entre colonnes (@dnd-kit avec React 19)
- [x] Drag & drop optimiste (mise à jour immédiate + rollback si erreur)
- [x] Filtrage par statut/priorité/assigné
- [x] Recherche en temps réel dans les tâches
- [x] Interface de filtrage complète (KanbanFilters.tsx)
- [x] Logique de filtrage dans TasksContext
- [x] Tri des tâches (date, priorité, alphabétique)
### 2.7 Système de thèmes (clair/sombre)
- [x] Créer le contexte de thème (ThemeContext + ThemeProvider)
- [x] Ajouter toggle de thème dans le Header (bouton avec icône soleil/lune)
- [x] Définir les variables CSS pour le thème clair
- [x] Adapter tous les composants UI pour supporter les deux thèmes
- [x] Modifier la palette de couleurs pour le mode clair
- [x] Adapter les composants Kanban (Board, TaskCard, Column)
- [x] Adapter les formulaires et modales
- [x] Adapter la page de gestion des tags
- [x] Sauvegarder la préférence de thème (localStorage)
- [x] Configuration par défaut selon préférence système (prefers-color-scheme)
## 📊 Phase 3: Intégrations et analytics (Priorité 3)
### 3.1 Gestion du Daily
- [x] Créer `services/daily.ts` - Service de gestion des daily notes
- [x] Modèle de données Daily (date, checkboxes hier/aujourd'hui)
- [x] Interface Daily avec sections "Hier" et "Aujourd'hui"
- [x] Checkboxes interactives avec état coché/non-coché
- [x] Liaison optionnelle checkbox ↔ tâche existante
- [x] Cocher une checkbox NE change PAS le statut de la tâche liée
- [x] Navigation par date (daily précédent/suivant)
- [x] Auto-création du daily du jour si inexistant
- [x] UX améliorée : édition au clic, focus persistant, input large
- [x] Vue calendar/historique des dailies
### 3.2 Intégration Jira Cloud
- [x] Créer `services/jira.ts` - Service de connexion à l'API Jira Cloud
- [x] Configuration Jira (URL, email, API token) dans `lib/config.ts`
- [x] Authentification Basic Auth (email + API token)
- [x] Récupération des tickets assignés à l'utilisateur
- [x] Mapping des statuts Jira vers statuts internes (todo, in_progress, done, etc.)
- [x] Synchronisation unidirectionnelle (Jira → local uniquement)
- [x] Gestion des diffs - ne pas écraser les modifications locales
- [x] Style visuel distinct pour les tâches Jira (bordure spéciale)
- [x] Métadonnées Jira (projet, clé, assignee) dans la base
- [x] Possibilité d'affecter des tags locaux aux tâches Jira
- [x] Interface de configuration dans les paramètres
- [x] Synchronisation manuelle via bouton (pas d'auto-sync)
- [x] Logs de synchronisation pour debug
- [x] Gestion des erreurs et timeouts API
### 3.3 Page d'accueil/dashboard
- [x] Créer une page d'accueil moderne avec vue d'ensemble
- [x] Widgets de statistiques (tâches par statut, priorité, etc.)
- [x] Déplacer kanban vers /kanban et créer nouveau dashboard à la racine
- [x] Actions rapides vers les différentes sections
- [x] Affichage des tâches récentes
- [x] Graphiques de productivité (tâches complétées par jour/semaine)
- [x] Indicateurs de performance personnels
- [x] Intégration des analytics dans le dashboard
### 3.4 Analytics et métriques
- [x] `services/analytics.ts` - Calculs statistiques
- [x] Métriques de productivité (vélocité, temps moyen, etc.)
- [x] Graphiques avec Recharts (tendances, vélocité, distribution)
- [x] Composants de graphiques (CompletionTrend, Velocity, Priority, Weekly)
- [x] Insights automatiques et métriques visuelles
## Autre Todo
- [x] Avoir un bouton pour réduire/agrandir la font des taches dans les kanban (swimlane et classique)
- [x] Refactorer les couleurs des priorités dans un seul endroit
- [x] Settings synchro Jira : ajouter une liste de projet à ignorer, doit etre pris en compte par le service bien sur
- [x] Faire des pages à part entière pour les sous-pages de la page config + SSR
- [x] Afficher dans l'édition de task les todo reliés. Pouvoir en ajouter directement avec une date ou sans.
- [x] Dans les titres de colonnes des swimlanes, je n'ai pas les couleurs des statuts
- [x] Système de sauvegarde automatique base de données
- [x] Sauvegarde automatique configurable (hourly/daily/weekly)
- [x] Configuration complète dans les paramètres avec interface dédiée
- [x] Rotation automatique des sauvegardes (configurable)
- [x] Format de sauvegarde avec timestamp + compression optionnelle
- [x] Interface complète pour visualiser et gérer les sauvegardes
- [x] CLI d'administration pour les opérations avancées
- [x] API REST complète pour la gestion programmatique
- [x] Vérification d'intégrité et restauration sécurisée
- [x] Option de restauration depuis une sauvegarde sélectionnée
## 🔧 Phase 4: Server Actions - Migration API Routes (Nouveau)
### 4.1 Migration vers Server Actions - Actions rapides
**Objectif** : Remplacer les API routes par des server actions pour les actions simples et fréquentes
#### Actions TaskCard (Priorité 1)
- [x] Créer `actions/tasks.ts` avec server actions de base
- [x] `updateTaskStatus(taskId, status)` - Changement de statut
- [x] `updateTaskTitle(taskId, title)` - Édition inline du titre
- [x] `deleteTask(taskId)` - Suppression de tâche
- [x] Modifier `TaskCard.tsx` pour utiliser server actions directement
- [x] Remplacer les props callbacks par calls directs aux actions
- [x] Intégrer `useTransition` pour les loading states natifs
- [x] Tester la revalidation automatique du cache
- [x] **Nettoyage** : Supprimer props obsolètes dans tous les composants Kanban
- [x] **Nettoyage** : Simplifier `tasks-client.ts` (garder GET et POST uniquement)
- [x] **Nettoyage** : Modifier `useTasks.ts` pour remplacer mutations par server actions
#### Actions Daily (Priorité 2)
- [x] Créer `actions/daily.ts` pour les checkboxes
- [x] `toggleCheckbox(checkboxId)` - Toggle état checkbox
- [x] `addCheckboxToDaily(dailyId, content)` - Ajouter checkbox
- [x] `updateCheckboxContent(checkboxId, content)` - Éditer contenu
- [x] `deleteCheckbox(checkboxId)` - Supprimer checkbox
- [x] `reorderCheckboxes(dailyId, checkboxIds)` - Réorganiser
- [x] Modifier les composants Daily pour utiliser server actions
- [x] **Nettoyage** : Supprimer routes `/api/daily/checkboxes` (POST, PATCH, DELETE)
- [x] **Nettoyage** : Simplifier `daily-client.ts` (garder GET uniquement)
- [x] **Nettoyage** : Modifier hook `useDaily.ts` pour `useTransition`
#### Actions User Preferences (Priorité 3)
- [x] Créer `actions/preferences.ts` pour les toggles
- [x] `updateViewPreferences(preferences)` - Préférences d'affichage
- [x] `updateKanbanFilters(filters)` - Filtres Kanban
- [x] `updateColumnVisibility(columns)` - Visibilité colonnes
- [x] `updateTheme(theme)` - Changement de thème
- [x] Remplacer les hooks par server actions directes
- [x] **Nettoyage** : Supprimer routes `/api/user-preferences/*` (PUT/PATCH)
- [x] **Nettoyage** : Simplifier `user-preferences-client.ts` (GET uniquement)
- [x] **Nettoyage** : Modifier `UserPreferencesContext.tsx` pour server actions
#### Actions Tags (Priorité 4)
- [x] Créer `actions/tags.ts` pour la gestion tags
- [x] `createTag(name, color)` - Création tag
- [x] `updateTag(tagId, data)` - Modification tag
- [x] `deleteTag(tagId)` - Suppression tag
- [x] Modifier les formulaires tags pour server actions
- [x] **Nettoyage** : Supprimer routes `/api/tags` (POST, PATCH, DELETE)
- [x] **Nettoyage** : Simplifier `tags-client.ts` (GET et search uniquement)
- [x] **Nettoyage** : Modifier `useTags.ts` pour server actions directes
#### Migration progressive avec nettoyage immédiat
**Principe** : Pour chaque action migrée → nettoyage immédiat des routes et code obsolètes
### 4.2 Conservation API Routes - Endpoints complexes
**À GARDER en API routes** (pas de migration)
#### Endpoints de fetching initial
-`GET /api/tasks` - Récupération avec filtres complexes
-`GET /api/daily` - Vue daily avec logique métier
-`GET /api/tags` - Liste tags avec recherche
-`GET /api/user-preferences` - Préférences initiales
#### Endpoints d'intégration externe
-`POST /api/jira/sync` - Synchronisation Jira complexe
-`GET /api/jira/logs` - Logs de synchronisation
- ✅ Configuration Jira (formulaires complexes)
#### Raisons de conservation
- **API publique** : Réutilisable depuis mobile/externe
- **Logique complexe** : Synchronisation, analytics, rapports
- **Monitoring** : Besoin de logs HTTP séparés
- **Real-time futur** : WebSockets/SSE non compatibles server actions
### 4.3 Architecture hybride cible
```
Actions rapides → Server Actions directes
├── TaskCard actions (status, title, delete)
├── Daily checkboxes (toggle, add, edit)
├── Preferences toggles (theme, filters)
└── Tags CRUD (create, update, delete)
Endpoints complexes → API Routes conservées
├── Fetching initial avec filtres
├── Intégrations externes (Jira, webhooks)
├── Analytics et rapports
└── Future real-time features
```
### 4.4 Avantages attendus
- **🚀 Performance** : Pas de sérialisation HTTP pour actions rapides
- **🔄 Cache intelligent** : `revalidatePath()` automatique
- **📦 Bundle reduction** : Moins de code client HTTP
- **⚡ UX** : `useTransition` loading states natifs
- **🎯 Simplicité** : Moins de boilerplate pour actions simples
## 📊 Phase 5: Surveillance Jira - Analytics d'équipe (Priorité 5)
### 5.1 Configuration projet Jira
- [x] Ajouter champ `projectKey` dans la config Jira (settings)
- [x] Interface pour sélectionner le projet à surveiller
- [x] Validation de l'existence du projet via API Jira
- [x] Sauvegarde de la configuration projet dans les préférences
- [x] Test de connexion spécifique au projet configuré
### 5.2 Service d'analytics Jira
- [x] Créer `services/jira-analytics.ts` - Métriques avancées
- [x] Récupération des tickets du projet (toute l'équipe, pas seulement assignés)
- [x] Calculs de vélocité d'équipe (story points par sprint)
- [x] Métriques de cycle time (temps entre statuts)
- [x] Analyse de la répartition des tâches par assignee
- [x] Détection des goulots d'étranglement (tickets bloqués)
- [x] Historique des sprints et burndown charts
- [x] Cache intelligent des métriques (éviter API rate limits)
### 5.3 Page de surveillance `/jira-dashboard`
- [x] Créer page dédiée avec navigation depuis settings Jira
- [x] Vue d'ensemble du projet (nom, lead, statut global)
- [x] Sélecteur de période (7j, 30j, 3 mois, sprint actuel)
- [x] Graphiques de vélocité avec Recharts
- [x] Heatmap d'activité de l'équipe
- [x] Timeline des releases et milestones
- [x] Alertes visuelles (tickets en retard, sprints déviants)
### 5.4 Métriques et graphiques avancés
- [x] **Vélocité** : Story points complétés par sprint
- [x] **Burndown chart** : Progression vs planifié
- [x] **Cycle time** : Temps moyen par type de ticket
- [x] **Throughput** : Nombre de tickets complétés par période
- [x] **Work in Progress** : Répartition par statut et assignee
- [x] **Quality metrics** : Ratio bugs/features, retours clients
- [x] **Predictability** : Variance entre estimé et réel
- [x] **Collaboration** : Matrice d'interactions entre assignees
### 5.5 Fonctionnalités de surveillance
- [x] **Cache serveur intelligent** : Cache en mémoire avec invalidation manuelle
- [x] **Export des métriques** : Export CSV/JSON avec téléchargement automatique
- [x] **Comparaison inter-sprints** : Tendances, prédictions et recommandations
- [x] Détection automatique d'anomalies (alertes)
- [x] Filtrage par composant, version, type de ticket
- [x] Vue détaillée par sprint avec drill-down
- [x] ~~Intégration avec les daily notes (mentions des blockers)~~ (supprimé)
## Autre Todos #2
- [ ] Synchro Jira auto en background timé comme pour la synchro de sauvegarde
- [ ] refacto des allpreferences : ca devrait eter un contexte dans le layout qui balance serverside dans le hook
## 🔧 Phase 6: Fonctionnalités avancées (Priorité 6)
### 6.1 Gestion avancée des tâches
- [ ] Actions en lot (sélection multiple)
- [ ] Sous-tâches et hiérarchie
- [ ] Dates d'échéance et rappels
- [ ] Assignation et collaboration
- [ ] Templates de tâches
### 6.2 Personnalisation et thèmes
- [ ] Mode sombre/clair
- [ ] Personnalisation des couleurs
- [ ] Configuration des colonnes Kanban
- [ ] Préférences utilisateur
## 🚀 Phase 7: Intégrations futures (Priorité 7)
### 7.1 Intégrations externes (optionnel)
- [ ] Import/Export depuis d'autres outils
- [ ] API webhooks pour intégrations
- [ ] Synchronisation cloud (optionnel)
- [ ] Notifications push
### 7.2 Optimisations et performance
- [ ] Optimisation des requêtes DB
- [ ] Pagination et virtualisation
- [ ] Cache côté client
- [ ] Optimisations Perf : requetes DB
- [ ] PWA et mode offline
## 🛠️ Configuration technique
### Stack moderne
- **Frontend**: Next.js 14, React, TypeScript, Tailwind CSS
- **Backend**: Next.js API Routes, Prisma ORM
- **Database**: SQLite (local) → PostgreSQL (production future)
- **UI**: Composants custom + Shadcn/ui, React Beautiful DnD
- **Charts**: Recharts ou Chart.js pour les analytics
### Architecture respectée
```
src/app/
├── api/tasks/ # API CRUD complète
├── page.tsx # Page principale
└── layout.tsx
services/
├── database.ts # Pool Prisma
└── tasks.ts # Service tâches standalone
components/
├── kanban/ # Board Kanban
├── ui/ # Composants UI de base
└── dashboard/ # Widgets dashboard (futur)
clients/ # Clients HTTP (à créer)
hooks/ # Hooks React (à créer)
lib/
├── types.ts # Types TypeScript
└── config.ts # Config app moderne
```
## 🎯 Prochaines étapes immédiates
1. **Drag & drop entre colonnes** - react-beautiful-dnd pour changer les statuts
2. **Gestion avancée des tags** - Couleurs, autocomplete, filtrage
3. **Recherche et filtres** - Filtrage temps réel par titre, tags, statut
4. **Dashboard et analytics** - Graphiques de productivité
## ✅ **Fonctionnalités terminées (Phase 2.1-2.3)**
- ✅ Système de design tech dark complet
- ✅ Composants UI de base (Button, Input, Card, Modal, Badge)
- ✅ Architecture SSR + hydratation client
- ✅ CRUD tâches complet (création, édition, suppression)
- ✅ Création rapide inline (QuickAddTask)
- ✅ Édition inline du titre (clic sur titre → input éditable)
- ✅ Drag & drop entre colonnes (@dnd-kit) + optimiste
- ✅ Client HTTP et hooks React
- ✅ Refactoring Kanban avec nouveaux composants
---
*Focus sur l'expérience utilisateur et le design moderne. App standalone prête pour évoluer.*
## 🐛 Problèmes relevés en réunion - Corrections UI/UX
### 🎨 Design et Interface
- [x] **Homepage cards** : toute en variant glass
- [x] **Icône Kanban homepage** - Changer icône sur la page d'accueil, pas lisible (utiliser une lib)
- [x] **Lisibilité label graph par tag** - Améliorer la lisibilité des labels dans les graphiques par tag <!-- Amélioré marges, légendes, tailles de police, retiré emojis -->
- [x] **Tag homepage** - Problème d'affichage des graphs de tags sur la homepage côté lisibilité, certaines icones ne sont pas entièrement visible, et la légende est trop proche du graphe. <!-- Amélioré hauteur, marges, responsive -->
- [x] **Tâches récentes** - Revoir l'affichage et la logique des tâches récentes <!-- Logique améliorée (tâches terminées récentes), responsive, icône claire -->
- [x] **Header dépasse en tablet** - Corriger le débordement du header sur tablette <!-- Responsive amélioré, taille réglée, navigation adaptative -->
- [x] **Icônes agenda et filtres** - Améliorer les icônes de l'agenda et des filtres dans desktop controls (utiliser une lib) <!-- Clock pour échéance, Settings pour filtres, Search visuelle -->
- [x] **Réunion/tâche design** - Revoir le design des bouton dans dailySectrion : les toggles avoir un compposant ui
- [x] **Légende calendrier et padding** - Corriger l'espacement et la légende du calendrier dans daily
- [x] **EditModal task couleur calendrier** - Problème de couleur en ajout de taches dans tous les icones calendriers; colmler au thème
- [x] **Weekly deux boutons actualiser** - Corriger la duplication des boutons d'actualisation
- [x] **Solarized ne doit pas être un soleil** - Corriger l'icône du thème Solarized
- [x] **Emoji interdit dans UI** - Vérifier et supprimer toutes les emojis dans l'interface, remplacer par lucide-react
- [ ] **Settings intégration : icônes** - Problème avec les icônes "Arrêté" et "Actif" : doivent etre les memes
- [ ] **Settings backup UI** - Revoir l'UI pour coller au style des intégrations
- [ ] **AlertBanner : hover et bug** - Corriger les problèmes de hover et bugs
- [ ] **Deux modales** - Problème de duplication de modales
- [ ] **Control panel et select** - Problème avec les contrôles et sélecteurs
- [ ] **TaskCard et Kanban transparence** - Appliquer la transparence sur le background et non sur la card
- [x] **Recherche Kanban desktop controls** - Ajouter icône et label : "rechercher" pour rapetir
- [ ] **Largeur page Kanban** - Réduire légèrement la largeur et revoir toutes les autres pages
- [x] **Icône thème à gauche du profil** - Repositionner l'icône de thème dans le header
- [ ] **Déconnexion trop petit et couleur** - Améliorer le bouton de déconnexion
- [ ] **Fond modal trop opaque** - Réduire l'opacité du fond des modales
- [ ] **Couleurs thème clair et TFS Jira Kanban** - Harmoniser les couleurs du thème clair
- [x] **États sélectionnés desktop control** - Revoir les couleurs des états sélectionnés pour avoir le joli bleu du dropdown partout
- [ ] **Dépasse 1000 caractères en edit modal task** - Corriger la limite (pas de limite) et revoir la quickcard description
- [ ] **UI si échéance et trop de labels dans le footer de card** - Améliorer l'affichage en mode détaillé TaskCard; certains boutons sont sur deux lignes ce qui casse l'affichage
- [ ] **Gravatar** - Implémenter l'affichage des avatars Gravatar
### 🔧 Fonctionnalités et Intégrations
- [ ] **Synchro Jira et TFS shortcuts** - Ajouter des raccourcis et bouton dans Kanban
- [x] **Intégration suppressions Jira/TFS** - Aligner la gestion des suppressions sur TFS, je veux que ce qu'on a récupéré dans la synchro, quand ca devient terminé dans Jira ou TFS, soit marqué comme terminé dans le Kanban et non supprimé du kanban. <!-- COMPLET: 1) JQL inclut resolved >= -30d pour récupérer tâches terminées, 2) syncSingleTask met à jour status + completedAt, 3) cleanupUnassignedTasks/cleanupInactivePullRequests préservent tâches done/archived -->
- [ ] **Log d'activité** - Implémenter un système de log d'activité (feature potentielle)
---
## 🚀 Nouvelles idées & fonctionnalités futures
### 🎯 Jira - Suivi des demandes en attente
- [ ] **Page "Jiras en attente"**
- [ ] Liste des Jiras créés par moi mais non assignés à mon équipe
- [ ] Suivi des demandes formulées à d'autres équipes
- [ ] Filtrage par projet, équipe cible, ancienneté
- [ ] **Nouveau modèle de données**
- [ ] Table séparée pour les "demandes en attente" (différent des tâches Kanban)
- [ ] Champs spécifiques : demandeur, équipe cible, statut de traitement
- [ ] Notifications quand une demande change de statut
### 👥 Gestion multi-utilisateurs (PROJET MAJEUR)
#### **Architecture actuelle → Multi-tenant**
- **Problème** : App mono-utilisateur avec données globales
- **Solution** : Transformation en app multi-utilisateurs avec isolation des données + système de rôles
#### **Plan de migration**
- [ ] **Phase 1: Authentification**
- [ ] Système de login/mot de passe (NextAuth.js)
- [ ] Gestion des sessions sécurisées
- [ ] Pages de connexion/inscription/mot de passe oublié
- [ ] Middleware de protection des routes
- [ ] **Phase 2: Modèle de données multi-tenant + Rôles**
- [ ] **Modèle User complet**
- [ ] Table `users` (id, email, password, name, role, createdAt, updatedAt)
- [ ] Enum `UserRole` : `ADMIN`, `MANAGER`, `USER`
- [ ] Champs optionnels : avatar, timezone, language
- [ ] **Relations hiérarchiques**
- [ ] Table `user_teams` pour les relations manager → users
- [ ] Champ `managerId` dans users (optionnel, référence vers un manager)
- [ ] Support des équipes multiples par utilisateur
- [ ] **Migration des données existantes**
- [ ] Créer un utilisateur admin par défaut avec toutes les données actuelles
- [ ] Ajouter `userId` à toutes les tables (tasks, daily, tags, preferences, etc.)
- [ ] Contraintes de base de données pour l'isolation
- [ ] Index sur `userId` pour les performances
- [ ] **Phase 3: Système de rôles et permissions**
- [ ] **Rôle ADMIN**
- [ ] Gestion complète des utilisateurs (CRUD)
- [ ] Assignation/modification des rôles
- [ ] Accès à toutes les données système (analytics globales)
- [ ] Configuration système (intégrations Jira/TFS globales)
- [ ] Gestion des équipes et hiérarchies
- [ ] **Rôle MANAGER**
- [ ] Vue sur les tâches/daily de ses équipiers
- [ ] Assignation de tâches à ses équipiers
- [ ] Analytics d'équipe (métriques, deadlines, performance)
- [ ] Création de tâches pour son équipe
- [ ] Accès aux rapports de son équipe
- [ ] **Rôle USER**
- [ ] Accès uniquement à ses propres données
- [ ] Réception de tâches assignées par son manager
- [ ] Gestion de son daily/kanban personnel
- [ ] **Middleware de permissions**
- [ ] Validation des droits d'accès par route
- [ ] Helper functions `canAccess()`, `canManage()`, `isAdmin()`
- [ ] Protection automatique des API routes
- [ ] **Phase 4: Services et API avec rôles**
- [ ] **Services utilisateurs**
- [ ] `user-management.ts` : CRUD utilisateurs (admin only)
- [ ] `team-management.ts` : Gestion des équipes (admin/manager)
- [ ] `role-permissions.ts` : Logique des permissions
- [ ] **Modification des services existants**
- [ ] Tous les services filtrent par `userId` OU permissions manager
- [ ] Middleware d'injection automatique du `userId` + `userRole`
- [ ] Services analytics étendus pour les managers
- [ ] Validation que chaque utilisateur ne voit que ses données autorisées
- [ ] **Phase 5: UI et UX multi-rôles**
- [ ] **Interface Admin**
- [ ] Page de gestion des utilisateurs (/admin/users)
- [ ] Création/modification/suppression d'utilisateurs
- [ ] Assignation des rôles et équipes
- [ ] Dashboard admin avec métriques globales
- [ ] **Interface Manager**
- [ ] Vue équipe avec tâches de tous les équipiers
- [ ] Assignation de tâches à l'équipe
- [ ] Dashboard manager avec analytics d'équipe
- [ ] Gestion des deadlines et priorités d'équipe
- [ ] **Interface commune**
- [ ] Header avec profil utilisateur, rôle et déconnexion
- [ ] Onboarding différencié par rôle
- [ ] Navigation adaptée aux permissions
- [ ] Indicateurs visuels du rôle actuel
- [ ] **Phase 6: Fonctionnalités collaboratives**
- [ ] **Assignation de tâches**
- [ ] Managers peuvent créer et assigner des tâches
- [ ] Notifications de nouvelles tâches assignées
- [ ] Suivi du statut des tâches assignées
- [ ] **Partage et visibilité**
- [ ] Tâches partagées entre équipiers
- [ ] Commentaires et collaboration sur les tâches
- [ ] Historique des modifications par utilisateur
#### **Considérations techniques**
- **Base de données** : Ajouter `userId` partout + contraintes
- **Sécurité** : Validation côté serveur de l'isolation des données
- **Performance** : Index sur `userId`, pagination pour gros volumes
- **Migration** : Script de migration des données existantes
---
## 🤖 Intégration IA avec Mistral (Phase IA)
### **Socle technique**
- [ ] **Phase 1: Infrastructure Mistral**
- [ ] Configuration du client Mistral local
- [ ] Service `mistral-client.ts` avec connexion au modèle local
- [ ] Configuration des endpoints et paramètres (température, tokens, etc.)
- [ ] Gestion des erreurs et timeouts
- [ ] Cache des réponses pour éviter les appels répétés
- [ ] **Système de prompts**
- [ ] Template engine pour les prompts structurés
- [ ] Prompts spécialisés par fonctionnalité (analyse, génération, classification)
- [ ] Versioning des prompts pour A/B testing
- [ ] Logging des interactions pour amélioration continue
- [ ] **Sécurité et performance**
- [ ] Rate limiting pour éviter la surcharge du modèle local
- [ ] Validation des inputs avant envoi au modèle
- [ ] Sanitization des réponses IA
- [ ] Monitoring des performances (latence, tokens utilisés)
- [ ] **Phase 2: Services IA développés avec les features**
- [ ] Services créés au fur et à mesure des besoins des fonctionnalités
- [ ] Pas de développement anticipé - implémentation juste-à-temps
- [ ] Architecture modulaire pour faciliter l'ajout de nouveaux services
- [ ] **Phase 3: Configuration et gestion de l'assistant**
- [ ] **Page de configuration IA (/settings/ai-assistant)**
- [ ] Configuration du modèle Mistral (endpoint, température, max tokens)
- [ ] Activation/désactivation des fonctionnalités IA par catégorie
- [ ] Paramètres de personnalisation (style de réponses, niveau d'agressivité)
- [ ] Configuration des seuils (confiance minimale, fréquence des suggestions)
- [ ] **Gestion des prompts personnalisés**
- [ ] Interface pour modifier les prompts par fonctionnalité
- [ ] Aperçu en temps réel des modifications
- [ ] Sauvegarde/restauration des configurations
- [ ] Templates de prompts prédéfinis
- [ ] **Monitoring et analytics IA**
- [ ] Dashboard des performances IA (latence, tokens utilisés, coût)
- [ ] Historique des interactions et taux de succès
- [ ] Métriques d'utilisation par fonctionnalité
- [ ] Logs des erreurs et suggestions d'amélioration
- [ ] **Système de feedback**
- [ ] Boutons "👍/👎" sur chaque suggestion IA
- [ ] Collecte des retours utilisateur pour amélioration
- [ ] A/B testing des différents prompts
- [ ] Apprentissage des préférences utilisateur
### **Fonctionnalités IA concrètes**
#### 🎯 **Smart Task Creation**
- [ ] **Bouton "Créer avec IA" dans le Kanban**
- [ ] Input libre : "Préparer présentation client pour vendredi"
- [ ] IA génère : titre, description, estimation durée, sous-tâches
- [ ] **Mapping prioritaire avec tags existants** : IA propose uniquement des tags déjà utilisés
- [ ] Validation/modification avant création
#### 🧠 **Daily Assistant**
- [ ] **Bouton "Smart Daily" dans la page Daily**
- [ ] Input libre : "Réunion client 14h, finir le rapport, appeler le fournisseur"
- [ ] IA génère une liste de checkboxes structurées
- [ ] Validation/modification avant ajout au Daily
- [ ] Pas de génération automatique - uniquement sur demande utilisateur
- [ ] **Smart Checkbox Suggestions**
- [ ] Pendant la saisie, IA propose des checkboxes similaires
#### 🎨 **Smart Tagging**
- [ ] **Auto-tagging des nouvelles tâches**
- [ ] IA analyse le titre/description
- [ ] Propose automatiquement 2-3 tags **existants** pertinents
- [ ] Apprentissage des tags utilisés par l'utilisateur
- [ ] **Suggestions de tags pendant la saisie**
- [ ] Dropdown intelligent avec **tags existants** probables uniquement
- [ ] Tri par fréquence d'usage et pertinence
#### 💬 **Chat Assistant**
- [ ] **Widget chat en bas à droite**
- [ ] "Quelles sont mes tâches urgentes cette semaine ?"
- [ ] "Comment optimiser mon planning demain ?"
- [ ] "Résume-moi mes performances de ce mois"
- [ ] **Recherche sémantique**
- [ ] "Tâches liées au projet X" même sans tag exact
- [ ] "Tâches que j'ai faites la semaine dernière"
- [ ] Recherche par contexte, pas juste mots-clés
#### 📈 **Smart Reports**
- [ ] **Génération automatique de rapports**
- [ ] Bouton "Générer rapport IA" dans analytics
- [ ] IA analyse les données et génère un résumé textuel
- [ ] Insights personnalisés ("Tu es plus productif le matin")
- [ ] **Alertes intelligentes**
- [ ] "Attention : tu as 3 tâches urgentes non démarrées"
- [ ] "Suggestion : regrouper les tâches similaires"
- [ ] Notifications contextuelles et actionables
#### ⚡ **Quick Actions**
- [ ] **Bouton "Optimiser" sur une tâche**
- [ ] IA suggère des améliorations (titre, description)
- [ ] Propose des **tags existants** pertinents
- [ ] Propose des sous-tâches manquantes
- [ ] Estimation de durée plus précise
- [ ] **Smart Duplicate Detection**
- [ ] "Cette tâche ressemble à une tâche existante"
- [ ] Suggestions de fusion ou différenciation
- [ ] Évite la duplication accidentelle
- [ ] **Exclusion des tâches avec tag "objectif principal"** : IA ignore ces tâches dans les comparaisons
---
_Focus sur l'expérience utilisateur et le design moderne. App standalone prête pour évoluer vers une plateforme d'intégration complète._

595
TODO_ARCHIVE.md Normal file
View File

@@ -0,0 +1,595 @@
# TowerControl v2.0 - Gestionnaire de tâches moderne
## ✅ Phase 1: Nettoyage et architecture (TERMINÉ)
### 1.1 Configuration projet Next.js
- [x] Initialiser Next.js avec TypeScript
- [x] Configurer ESLint, Prettier
- [x] Setup structure de dossiers selon les règles du workspace
- [x] Configurer base de données (SQLite local)
- [x] Setup Prisma ORM
### 1.2 Architecture backend standalone
- [x] Créer `services/database.ts` - Pool de connexion DB
- [x] Créer `services/tasks.ts` - Service CRUD pour les tâches
- [x] Créer `lib/types.ts` - Types partagés (Task, Tag, etc.)
- [x] Nettoyer l'ancien code de synchronisation
### 1.3 API moderne et propre
- [x] `app/api/tasks/route.ts` - API CRUD complète (GET, POST, PATCH, DELETE)
- [x] Supprimer les routes de synchronisation obsolètes
- [x] Configuration moderne dans `lib/config.ts`
**Architecture finale** : App standalone avec backend propre et API REST moderne
## 🎯 Phase 2: Interface utilisateur moderne (EN COURS)
### 2.1 Système de design et composants UI
- [x] Créer les composants UI de base (Button, Input, Card, Modal, Badge)
- [x] Implémenter le système de design tech dark (couleurs, typographie, spacing)
- [x] Setup Tailwind CSS avec classes utilitaires personnalisées
- [x] Créer une palette de couleurs tech/cyberpunk
### 2.2 Composants Kanban existants (à améliorer)
- [x] `components/kanban/Board.tsx` - Tableau Kanban principal
- [x] `components/kanban/Column.tsx` - Colonnes du Kanban
- [x] `components/kanban/TaskCard.tsx` - Cartes de tâches
- [x] `components/ui/Header.tsx` - Header avec statistiques
- [x] Refactoriser les composants pour utiliser le nouveau système UI
### 2.3 Gestion des tâches (CRUD)
- [x] Formulaire de création de tâche (Modal + Form)
- [x] Création rapide inline dans les colonnes (QuickAddTask)
- [x] Formulaire d'édition de tâche (Modal + Form avec pré-remplissage)
- [x] Édition inline du titre des tâches (clic sur titre → input)
- [x] Suppression de tâche (icône discrète + API call)
- [x] Changement de statut par drag & drop (@dnd-kit)
- [x] Validation des formulaires et gestion d'erreurs
### 2.4 Gestion des tags
- [x] Créer/éditer des tags avec sélecteur de couleur
- [x] Autocomplete pour les tags existants
- [x] Suppression de tags (avec vérification des dépendances)
- [x] Affichage des tags avec couleurs personnalisées
- [x] Service tags avec CRUD complet (Prisma)
- [x] API routes /api/tags avec validation
- [x] Client HTTP et hook useTags
- [x] Composants UI (TagInput, TagDisplay, TagForm)
- [x] Intégration dans les formulaires (TagInput avec autocomplete)
- [x] Intégration dans les TaskCards (TagDisplay avec couleurs)
- [x] Contexte global pour partager les tags
- [x] Page de gestion des tags (/tags) avec interface complète
- [x] Navigation dans le Header (Kanban ↔ Tags)
- [x] Filtrage par tags (intégration dans Kanban)
- [x] Interface de filtrage complète (recherche, priorités, tags)
- [x] Logique de filtrage temps réel dans le contexte
- [x] Intégration des filtres dans KanbanBoard
### 2.5 Clients HTTP et hooks
- [x] `clients/tasks-client.ts` - Client pour les tâches (CRUD complet)
- [x] `clients/tags-client.ts` - Client pour les tags
- [x] `clients/base/http-client.ts` - Client HTTP de base
- [x] `hooks/useTasks.ts` - Hook pour la gestion des tâches (CRUD complet)
- [x] `hooks/useTags.ts` - Hook pour la gestion des tags
- [x] Drag & drop avec @dnd-kit (intégré directement dans Board.tsx)
- [x] Gestion des erreurs et loading states
- [x] Architecture SSR + hydratation client optimisée
### 2.6 Fonctionnalités Kanban avancées
- [x] Drag & drop entre colonnes (@dnd-kit avec React 19)
- [x] Drag & drop optimiste (mise à jour immédiate + rollback si erreur)
- [x] Filtrage par statut/priorité/assigné
- [x] Recherche en temps réel dans les tâches
- [x] Interface de filtrage complète (KanbanFilters.tsx)
- [x] Logique de filtrage dans TasksContext
- [x] Tri des tâches (date, priorité, alphabétique)
### 2.7 Système de thèmes (clair/sombre)
- [x] Créer le contexte de thème (ThemeContext + ThemeProvider)
- [x] Ajouter toggle de thème dans le Header (bouton avec icône soleil/lune)
- [x] Définir les variables CSS pour le thème clair
- [x] Adapter tous les composants UI pour supporter les deux thèmes
- [x] Modifier la palette de couleurs pour le mode clair
- [x] Adapter les composants Kanban (Board, TaskCard, Column)
- [x] Adapter les formulaires et modales
- [x] Adapter la page de gestion des tags
- [x] Sauvegarder la préférence de thème (localStorage)
- [x] Configuration par défaut selon préférence système (prefers-color-scheme)
## 📊 Phase 3: Intégrations et analytics (Priorité 3)
### 3.1 Gestion du Daily
- [x] Créer `services/daily.ts` - Service de gestion des daily notes
- [x] Modèle de données Daily (date, checkboxes hier/aujourd'hui)
- [x] Interface Daily avec sections "Hier" et "Aujourd'hui"
- [x] Checkboxes interactives avec état coché/non-coché
- [x] Liaison optionnelle checkbox ↔ tâche existante
- [x] Cocher une checkbox NE change PAS le statut de la tâche liée
- [x] Navigation par date (daily précédent/suivant)
- [x] Auto-création du daily du jour si inexistant
- [x] UX améliorée : édition au clic, focus persistant, input large
- [x] Vue calendar/historique des dailies
### 3.2 Intégration Jira Cloud
- [x] Créer `services/jira.ts` - Service de connexion à l'API Jira Cloud
- [x] Configuration Jira (URL, email, API token) dans `lib/config.ts`
- [x] Authentification Basic Auth (email + API token)
- [x] Récupération des tickets assignés à l'utilisateur
- [x] Mapping des statuts Jira vers statuts internes (todo, in_progress, done, etc.)
- [x] Synchronisation unidirectionnelle (Jira → local uniquement)
- [x] Gestion des diffs - ne pas écraser les modifications locales
- [x] Style visuel distinct pour les tâches Jira (bordure spéciale)
- [x] Métadonnées Jira (projet, clé, assignee) dans la base
- [x] Possibilité d'affecter des tags locaux aux tâches Jira
- [x] Interface de configuration dans les paramètres
- [x] Synchronisation manuelle via bouton (pas d'auto-sync)
- [x] Logs de synchronisation pour debug
- [x] Gestion des erreurs et timeouts API
### 3.3 Page d'accueil/dashboard
- [x] Créer une page d'accueil moderne avec vue d'ensemble
- [x] Widgets de statistiques (tâches par statut, priorité, etc.)
- [x] Déplacer kanban vers /kanban et créer nouveau dashboard à la racine
- [x] Actions rapides vers les différentes sections
- [x] Affichage des tâches récentes
- [x] Graphiques de productivité (tâches complétées par jour/semaine)
- [x] Indicateurs de performance personnels
- [x] Intégration des analytics dans le dashboard
### 3.4 Analytics et métriques
- [x] `services/analytics.ts` - Calculs statistiques
- [x] Métriques de productivité (vélocité, temps moyen, etc.)
- [x] Graphiques avec Recharts (tendances, vélocité, distribution)
- [x] Composants de graphiques (CompletionTrend, Velocity, Priority, Weekly)
- [x] Insights automatiques et métriques visuelles
## Autre Todo
- [x] Avoir un bouton pour réduire/agrandir la font des taches dans les kanban (swimlane et classique)
- [x] Refactorer les couleurs des priorités dans un seul endroit
- [x] Settings synchro Jira : ajouter une liste de projet à ignorer, doit etre pris en compte par le service bien sur
- [x] Faire des pages à part entière pour les sous-pages de la page config + SSR
- [x] Afficher dans l'édition de task les todo reliés. Pouvoir en ajouter directement avec une date ou sans.
- [x] Dans les titres de colonnes des swimlanes, je n'ai pas les couleurs des statuts
- [x] Système de sauvegarde automatique base de données
- [x] Sauvegarde automatique configurable (hourly/daily/weekly)
- [x] Configuration complète dans les paramètres avec interface dédiée
- [x] Rotation automatique des sauvegardes (configurable)
- [x] Format de sauvegarde avec timestamp + compression optionnelle
- [x] Interface complète pour visualiser et gérer les sauvegardes
- [x] CLI d'administration pour les opérations avancées
- [x] API REST complète pour la gestion programmatique
- [x] Vérification d'intégrité et restauration sécurisée
- [x] Option de restauration depuis une sauvegarde sélectionnée
## 🔧 Phase 4: Server Actions - Migration API Routes (Nouveau)
### 4.1 Migration vers Server Actions - Actions rapides
**Objectif** : Remplacer les API routes par des server actions pour les actions simples et fréquentes
#### Actions TaskCard (Priorité 1)
- [x] Créer `actions/tasks.ts` avec server actions de base
- [x] `updateTaskStatus(taskId, status)` - Changement de statut
- [x] `updateTaskTitle(taskId, title)` - Édition inline du titre
- [x] `deleteTask(taskId)` - Suppression de tâche
- [x] Modifier `TaskCard.tsx` pour utiliser server actions directement
- [x] Remplacer les props callbacks par calls directs aux actions
- [x] Intégrer `useTransition` pour les loading states natifs
- [x] Tester la revalidation automatique du cache
- [x] **Nettoyage** : Supprimer props obsolètes dans tous les composants Kanban
- [x] **Nettoyage** : Simplifier `tasks-client.ts` (garder GET et POST uniquement)
- [x] **Nettoyage** : Modifier `useTasks.ts` pour remplacer mutations par server actions
#### Actions Daily (Priorité 2)
- [x] Créer `actions/daily.ts` pour les checkboxes
- [x] `toggleCheckbox(checkboxId)` - Toggle état checkbox
- [x] `addCheckboxToDaily(dailyId, content)` - Ajouter checkbox
- [x] `updateCheckboxContent(checkboxId, content)` - Éditer contenu
- [x] `deleteCheckbox(checkboxId)` - Supprimer checkbox
- [x] `reorderCheckboxes(dailyId, checkboxIds)` - Réorganiser
- [x] Modifier les composants Daily pour utiliser server actions
- [x] **Nettoyage** : Supprimer routes `/api/daily/checkboxes` (POST, PATCH, DELETE)
- [x] **Nettoyage** : Simplifier `daily-client.ts` (garder GET uniquement)
- [x] **Nettoyage** : Modifier hook `useDaily.ts` pour `useTransition`
#### Actions User Preferences (Priorité 3)
- [x] Créer `actions/preferences.ts` pour les toggles
- [x] `updateViewPreferences(preferences)` - Préférences d'affichage
- [x] `updateKanbanFilters(filters)` - Filtres Kanban
- [x] `updateColumnVisibility(columns)` - Visibilité colonnes
- [x] `updateTheme(theme)` - Changement de thème
- [x] Remplacer les hooks par server actions directes
- [x] **Nettoyage** : Supprimer routes `/api/user-preferences/*` (PUT/PATCH)
- [x] **Nettoyage** : Simplifier `user-preferences-client.ts` (GET uniquement)
- [x] **Nettoyage** : Modifier `UserPreferencesContext.tsx` pour server actions
#### Actions Tags (Priorité 4)
- [x] Créer `actions/tags.ts` pour la gestion tags
- [x] `createTag(name, color)` - Création tag
- [x] `updateTag(tagId, data)` - Modification tag
- [x] `deleteTag(tagId)` - Suppression tag
- [x] Modifier les formulaires tags pour server actions
- [x] **Nettoyage** : Supprimer routes `/api/tags` (POST, PATCH, DELETE)
- [x] **Nettoyage** : Simplifier `tags-client.ts` (GET et search uniquement)
- [x] **Nettoyage** : Modifier `useTags.ts` pour server actions directes
#### Migration progressive avec nettoyage immédiat
**Principe** : Pour chaque action migrée → nettoyage immédiat des routes et code obsolètes
### 4.2 Conservation API Routes - Endpoints complexes
**À GARDER en API routes** (pas de migration)
#### Endpoints de fetching initial
-`GET /api/tasks` - Récupération avec filtres complexes
-`GET /api/daily` - Vue daily avec logique métier
-`GET /api/tags` - Liste tags avec recherche
-`GET /api/user-preferences` - Préférences initiales
#### Endpoints d'intégration externe
-`POST /api/jira/sync` - Synchronisation Jira complexe
-`GET /api/jira/logs` - Logs de synchronisation
- ✅ Configuration Jira (formulaires complexes)
#### Raisons de conservation
- **API publique** : Réutilisable depuis mobile/externe
- **Logique complexe** : Synchronisation, analytics, rapports
- **Monitoring** : Besoin de logs HTTP séparés
- **Real-time futur** : WebSockets/SSE non compatibles server actions
### 4.3 Architecture hybride cible
```
Actions rapides → Server Actions directes
├── TaskCard actions (status, title, delete)
├── Daily checkboxes (toggle, add, edit)
├── Preferences toggles (theme, filters)
└── Tags CRUD (create, update, delete)
Endpoints complexes → API Routes conservées
├── Fetching initial avec filtres
├── Intégrations externes (Jira, webhooks)
├── Analytics et rapports
└── Future real-time features
```
### 4.4 Avantages attendus
- **🚀 Performance** : Pas de sérialisation HTTP pour actions rapides
- **🔄 Cache intelligent** : `revalidatePath()` automatique
- **📦 Bundle reduction** : Moins de code client HTTP
- **⚡ UX** : `useTransition` loading states natifs
- **🎯 Simplicité** : Moins de boilerplate pour actions simples
## 📊 Phase 5: Surveillance Jira - Analytics d'équipe (Priorité 5)
### 5.1 Configuration projet Jira
- [x] Ajouter champ `projectKey` dans la config Jira (settings)
- [x] Interface pour sélectionner le projet à surveiller
- [x] Validation de l'existence du projet via API Jira
- [x] Sauvegarde de la configuration projet dans les préférences
- [x] Test de connexion spécifique au projet configuré
### 5.2 Service d'analytics Jira
- [x] Créer `services/jira-analytics.ts` - Métriques avancées
- [x] Récupération des tickets du projet (toute l'équipe, pas seulement assignés)
- [x] Calculs de vélocité d'équipe (story points par sprint)
- [x] Métriques de cycle time (temps entre statuts)
- [x] Analyse de la répartition des tâches par assignee
- [x] Détection des goulots d'étranglement (tickets bloqués)
- [x] Historique des sprints et burndown charts
- [x] Cache intelligent des métriques (éviter API rate limits)
### 5.3 Page de surveillance `/jira-dashboard`
- [x] Créer page dédiée avec navigation depuis settings Jira
- [x] Vue d'ensemble du projet (nom, lead, statut global)
- [x] Sélecteur de période (7j, 30j, 3 mois, sprint actuel)
- [x] Graphiques de vélocité avec Recharts
- [x] Heatmap d'activité de l'équipe
- [x] Timeline des releases et milestones
- [x] Alertes visuelles (tickets en retard, sprints déviants)
### 5.4 Métriques et graphiques avancés
- [x] **Vélocité** : Story points complétés par sprint
- [x] **Burndown chart** : Progression vs planifié
- [x] **Cycle time** : Temps moyen par type de ticket
- [x] **Throughput** : Nombre de tickets complétés par période
- [x] **Work in Progress** : Répartition par statut et assignee
- [x] **Quality metrics** : Ratio bugs/features, retours clients
- [x] **Predictability** : Variance entre estimé et réel
- [x] **Collaboration** : Matrice d'interactions entre assignees
### 5.5 Fonctionnalités de surveillance
- [x] **Cache serveur intelligent** : Cache en mémoire avec invalidation manuelle
- [x] **Export des métriques** : Export CSV/JSON avec téléchargement automatique
- [x] **Comparaison inter-sprints** : Tendances, prédictions et recommandations
- [x] Détection automatique d'anomalies (alertes)
- [x] Filtrage par composant, version, type de ticket
- [x] Vue détaillée par sprint avec drill-down
- [x] ~~Intégration avec les daily notes (mentions des blockers)~~ (supprimé)
### 📁 Refactoring structure des dossiers (PRIORITÉ HAUTE)
#### **Problème actuel**
- Structure mixte : `src/app/`, `src/actions/`, `src/contexts/` mais `components/`, `lib/`, `services/`, etc. à la racine
- Alias TypeScript incohérents dans `tsconfig.json`
- Non-conformité avec les bonnes pratiques Next.js 13+ App Router
#### **Plan de migration**
- [x] **Phase 1: Migration des dossiers**
- [x] `mv components/ src/components/`
- [x] `mv lib/ src/lib/`
- [x] `mv hooks/ src/hooks/`
- [x] `mv clients/ src/clients/`
- [x] `mv services/ src/services/`
- [x] **Phase 2: Mise à jour tsconfig.json**
```json
"paths": {
"@/*": ["./src/*"]
// Supprimer les alias spécifiques devenus inutiles
}
```
- [x] **Phase 3: Correction des imports**
- [x] Tous les imports `@/services/*` → `@/services/*` (déjà OK)
- [x] Tous les imports `@/lib/*` → `@/lib/*` (déjà OK)
- [x] Tous les imports `@/components/*` → `@/components/*` (déjà OK)
- [x] Tous les imports `@/clients/*` → `@/clients/*` (déjà OK)
- [x] Tous les imports `@/hooks/*` → `@/hooks/*` (déjà OK)
- [x] Vérifier les imports relatifs dans les scripts/
- [x] **Phase 4: Mise à jour des règles Cursor**
- [x] Règle "services" : Mettre à jour les exemples avec `src/services/`
- [x] Règle "components" : Mettre à jour avec `src/components/`
- [x] Règle "clients" : Mettre à jour avec `src/clients/`
- [x] Vérifier tous les liens MDC dans les règles
- [x] **Phase 5: Tests et validation**
- [x] `npm run build` - Vérifier que le build passe
- [x] `npm run dev` - Vérifier que le dev fonctionne
- [x] `npm run lint` - Vérifier ESLint
- [x] `npx tsc --noEmit` - Vérifier TypeScript
- [x] Tester les fonctionnalités principales
#### **Structure finale attendue**
```
src/
├── app/ # Pages Next.js (déjà OK)
├── actions/ # Server Actions (déjà OK)
├── contexts/ # React Contexts (déjà OK)
├── components/ # Composants React (à déplacer)
├── lib/ # Utilitaires et types (à déplacer)
├── hooks/ # Hooks React (à déplacer)
├── clients/ # Clients HTTP (à déplacer)
└── services/ # Services backend (à déplacer)
## Autre Todos
- [x] Synchro Jira auto en background timé comme pour la synchro de sauvegarde
- [x] refacto des getallpreferences en frontend : ca devrait eter un contexte dans le layout qui balance serverside dans le hook
- [x] backups : ne backuper que si il y a eu un changement entre le dernier backup et la base actuelle
- [x] refacto des dates avec le utils qui pour l'instant n'est pas utilisé
- [x] split de certains gros composants.
- [x] Page jira-dashboard : onglets analytics avancés et Qualité et collaboration : les charts sortent des cards; il faut reprendre la UI pour que ce soit consistant.
- [x] Page Daily : les mots aujourd'hui et hier ne fonctionnent dans les titres que si c'est vraiment aujourd'hui :)
- [x] Désactiver le hover sur les taskCard
- [x] Refacto et intégration design : mode sombre et clair sont souvent mal généré par défaut
- [x] Personnalisation : couleurs <!-- Image de fond personnalisée implémentée -->
## 🔄 Refactoring Services par Domaine
### Organisation cible des services:
```
src/services/
├── core/ # Services fondamentaux
├── analytics/ # Analytics et métriques
├── data-management/# Backup, système, base
├── integrations/ # Services externes
├── task-management/# Gestion des tâches
```
### Phase 1: Services Core (infrastructure) ✅
- [x] **Déplacer `database.ts`** → `core/database.ts`
- [x] Corriger tous les imports internes des services
- [x] Corriger import dans scripts/reset-database.ts
- [x] **Déplacer `system-info.ts`** → `core/system-info.ts`
- [x] Corriger imports dans actions/system
- [x] Corriger import dynamique de backup
- [x] **Déplacer `user-preferences.ts`** → `core/user-preferences.ts`
- [x] Corriger 13 imports externes (actions, API routes, pages)
- [x] Corriger 3 imports internes entre services
### Phase 2: Analytics & Métriques ✅
- [x] **Déplacer `analytics.ts`** → `analytics/analytics.ts`
- [x] Corriger 2 imports externes (actions, components)
- [x] **Déplacer `metrics.ts`** → `analytics/metrics.ts`
- [x] Corriger 7 imports externes (actions, hooks, components)
- [x] **Déplacer `manager-summary.ts`** → `analytics/manager-summary.ts`
- [x] Corriger 3 imports externes (components, pages)
- [x] Corriger imports database vers ../core/database
### Phase 3: Data Management ✅
- [x] **Déplacer `backup.ts`** → `data-management/backup.ts`
- [x] Corriger 6 imports externes (clients, components, pages, API)
- [x] Corriger imports relatifs vers ../core/ et ../../lib/
- [x] **Déplacer `backup-scheduler.ts`** → `data-management/backup-scheduler.ts`
- [x] Corriger import dans script backup-manager.ts
- [x] Corriger imports relatifs entre services
### Phase 4: Task Management ✅
- [x] **Déplacer `tasks.ts`** → `task-management/tasks.ts`
- [x] Corriger 7 imports externes (pages, API routes, actions)
- [x] Corriger import dans script seed-data.ts
- [x] **Déplacer `tags.ts`** → `task-management/tags.ts`
- [x] Corriger 8 imports externes (pages, API routes, actions)
- [x] Corriger import dans script seed-tags.ts
- [x] **Déplacer `daily.ts`** → `task-management/daily.ts`
- [x] Corriger 6 imports externes (pages, API routes, actions)
- [x] Corriger imports relatifs vers ../core/database
### Phase 5: Intégrations ✅
- [x] **Déplacer `tfs.ts`** → `integrations/tfs.ts`
- [x] Corriger 10 imports externes (actions, API routes, components, types)
- [x] Corriger imports relatifs vers ../core/
- [x] **Déplacer services Jira** → `integrations/jira/`
- [x] `jira.ts` → `integrations/jira/jira.ts`
- [x] `jira-scheduler.ts` → `integrations/jira/scheduler.ts`
- [x] `jira-analytics.ts` → `integrations/jira/analytics.ts`
- [x] `jira-analytics-cache.ts` → `integrations/jira/analytics-cache.ts`
- [x] `jira-advanced-filters.ts` → `integrations/jira/advanced-filters.ts`
- [x] `jira-anomaly-detection.ts` → `integrations/jira/anomaly-detection.ts`
- [x] Corriger 18 imports externes (actions, API routes, hooks, components)
- [x] Corriger imports relatifs entre services Jira
## Phase 6: Cleaning
- [x] **Uniformiser les imports absolus** dans tous les services
- [x] Remplacer tous les imports relatifs `../` par `@/services/...`
- [x] Corriger l'import dynamique dans system-info.ts
- [x] 12 imports relatifs → imports absolus cohérents
### Points d'attention pour chaque service:
1. **Identifier tous les imports du service** (grep)
2. **Déplacer le fichier** vers le nouveau dossier
3. **Corriger les imports externes** (actions, API, hooks, components)
4. **Corriger les imports internes** entre services
5. **Tester** que l'app fonctionne toujours
6. **Commit** le déplacement d'un service à la fois
```
### 🔄 Intégration TFS/Azure DevOps
- [x] **Lecture des Pull Requests TFS** : Synchronisation des PR comme tâches <!-- Implémenté le 22/09/2025 -->
- [x] PR arrivent en backlog avec filtrage par team project
- [x] Synchronisation aussi riche que Jira (statuts, assignés, commentaires)
- [x] Filtrage par team project, repository, auteur
- [x] **Architecture plug-and-play pour intégrations** <!-- Implémenté le 22/09/2025 -->
- [x] Refactoriser pour interfaces génériques d'intégration
- [x] Interface `IntegrationService` commune (Jira, TFS, GitHub, etc.)
- [x] UI générique de configuration des intégrations
- [x] Système de plugins pour ajouter facilement de nouveaux services
### 📋 Daily - Gestion des tâches non cochées
- [x] **Section des tâches en attente** <!-- Implémenté le 21/09/2025 -->
- [x] Liste de toutes les todos non cochées (historique complet)
- [x] Filtrage par date (7/14/30 jours), catégorie (tâches/réunions), ancienneté
- [x] Action "Archiver" pour les tâches ni résolues ni à faire
- [x] Section repliable dans la page Daily (sous les sections Hier/Aujourd'hui)
- [x] **Bouton "Déplacer à aujourd'hui"** pour les tâches non résolues <!-- Implémenté le 22/09/2025 avec server action -->
- [x] Indicateurs visuels d'ancienneté (couleurs vert→rouge)
- [x] Actions par tâche : Cocher, Archiver, Supprimer
- [x] **Statut "Archivé" basique** <!-- Implémenté le 21/09/2025 -->
- [x] Marquage textuel [ARCHIVÉ] dans le texte de la tâche
- [x] Interface pour voir les tâches archivées (visuellement distinctes)
- [ ] Possibilité de désarchiver une tâche
- [ ] Champ dédié en base de données (actuellement via texte)
---
## 🖼️ **IMAGE DE FOND PERSONNALISÉE** ✅ TERMINÉ
### **Fonctionnalités implémentées :**
- [x] **Sélecteur d'images de fond** dans les paramètres généraux
- [x] **Images prédéfinies** : dégradés bleu, violet, coucher de soleil, océan, forêt
- [x] **URL personnalisée** : possibilité d'ajouter une image via URL
- [x] **Aperçu en temps réel** de l'image sélectionnée
- [x] **Application globale** : l'image s'applique sur toutes les pages
- [x] **Optimisation visuelle** : effet de flou et transparence pour la lisibilité
- [x] **Sauvegarde persistante** : préférence sauvegardée en base de données
- [x] **Interface intuitive** : sélection facile avec aperçus visuels
### **Architecture technique :**
- **Types** : `backgroundImage` ajouté à `ViewPreferences`
- **Service** : `userPreferencesService` mis à jour
- **Actions** : `setBackgroundImage` server action créée
- **Composant** : `BackgroundImageSelector` avec presets et URL personnalisée
- **Contexte** : `BackgroundContext` pour l'application globale
- **Styles** : CSS optimisé pour la lisibilité avec images de fond
## 🔄 **SCHEDULER TFS** ✅ TERMINÉ
### **Fonctionnalités implémentées :**
- [x] **Scheduler TFS automatique** basé sur le modèle Jira
- [x] **Configuration dans UserPreferences** : `tfsAutoSync` et `tfsSyncInterval`
- [x] **Intervalles configurables** : hourly, daily, weekly
- [x] **Auto-start du scheduler** au démarrage de l'application
- [x] **Migration douce** des champs scheduler en base de données
- [x] **Gestion des erreurs** et validation de configuration
- [x] **Status et monitoring** du scheduler
### **Architecture technique :**
- **Service** : `TfsScheduler` dans `src/services/integrations/tfs/scheduler.ts`
- **Configuration** : Champs `tfsAutoSync` et `tfsSyncInterval` dans `UserPreferences`
- **Migration** : Méthode `ensureTfsSchedulerFields()` pour compatibilité
- **Types** : Interface `TfsSchedulerConfig` avec validation
- **Singleton** : Instance globale `tfsScheduler` avec auto-start
- **Logs** : Console logs détaillés pour monitoring
### **Différences avec Jira :**
- **Pas de board d'équipe** : TFS se concentre sur les Pull Requests individuelles
- **Configuration simplifiée** : Pas de `ignoredProjects`, mais `ignoredRepositories`
- **Focus utilisateur** : Synchronisation basée sur les PRs assignées à l'utilisateur
### **Interface utilisateur :**
- **TfsSchedulerConfig** : Configuration du scheduler automatique avec statut et contrôles
- **TfsSync** : Interface de synchronisation manuelle avec détails et statistiques
- **API Routes** : `/api/tfs/scheduler-config` et `/api/tfs/scheduler-status` pour la gestion
- **Même format que Jira** : Interface identique avec badges de statut, contrôles et informations
---
## 🎨 **REFACTORING THÈME & PERSONNALISATION COULEURS**
### **Phase 1: Nettoyage Architecture Thème**
- [x] **Décider de la stratégie** : CSS Variables vs Tailwind Dark Mode vs Hybride <!-- CSS Variables choisi -->
- [x] **Configurer tailwind.config.js** avec `darkMode: 'class'` si nécessaire <!-- Annulé : CSS Variables pur -->
- [x] **Supprimer la double application** du thème (layout.tsx + ThemeContext + UserPreferencesContext) <!-- ThemeContext est maintenant la source unique -->
- [x] **Refactorer les CSS variables** : `:root` pour défaut, `.dark/.light` pour override <!-- Architecture CSS propre avec :root neutre -->
- [x] **Nettoyer les composants** : supprimer classes `dark:` hardcodées, utiliser uniquement CSS variables <!-- TERMINÉ : toutes les occurrences supprimées -->
- [ ] **Corriger les problèmes d'hydration** mismatch et flashs de thème
- [ ] **Créer un système de design cohérent** avec tokens de couleur
---

125
UI_COMPONENTS_GUIDE.md Normal file
View File

@@ -0,0 +1,125 @@
# Guide des Composants UI
## 🎯 Principe
**Les composants métier ne doivent JAMAIS utiliser directement les variables CSS.** Ils doivent utiliser les composants UI abstraits.
## ❌ MAUVAIS
```tsx
// ❌ Composant métier avec variables CSS directes
function TaskCard({ task }) {
return (
<div className="bg-[var(--card)] border border-[var(--border)] p-4 rounded-lg">
<button className="bg-[var(--primary)] text-[var(--primary-foreground)] px-4 py-2 rounded">
{task.title}
</button>
</div>
);
}
```
## ✅ BON
```tsx
// ✅ Composant métier utilisant les composants UI
import { Card, CardContent, Button } from '@/components/ui';
function TaskCard({ task }) {
return (
<Card>
<CardContent>
<Button variant="primary">{task.title}</Button>
</CardContent>
</Card>
);
}
```
## 📦 Composants UI Disponibles
### Button
```tsx
<Button variant="primary" size="md">Action</Button>
<Button variant="secondary">Secondaire</Button>
<Button variant="destructive">Supprimer</Button>
<Button variant="ghost">Ghost</Button>
```
### Badge
```tsx
<Badge variant="primary">Tag</Badge>
<Badge variant="success">Succès</Badge>
<Badge variant="destructive">Erreur</Badge>
```
### Alert
```tsx
<Alert variant="success">
<AlertTitle>Succès</AlertTitle>
<AlertDescription>Opération réussie</AlertDescription>
</Alert>
```
### Input
```tsx
<Input placeholder="Saisir..." />
<Input variant="error" placeholder="Erreur" />
```
### StyledCard
```tsx
<StyledCard variant="outline" color="primary">
Contenu avec style coloré
</StyledCard>
```
### Avatar
```tsx
// Avatar avec URL personnalisée
<Avatar url="https://example.com/photo.jpg" email="user@example.com" name="John Doe" size={64} />
// Avatar Gravatar automatique (si pas d'URL fournie)
<Avatar email="user@gravatar.com" name="Jane Doe" size={48} />
// Avatar avec fallback
<Avatar email="unknown@example.com" name="Unknown User" size={32} />
```
## 🔄 Migration
### Étape 1: Identifier les patterns
- Rechercher `var(--` dans les composants métier
- Identifier les patterns répétés (boutons, cartes, badges)
### Étape 2: Créer des composants UI
- Encapsuler les styles dans des composants UI
- Utiliser des variants pour les variations
### Étape 3: Remplacer dans les composants métier
- Importer les composants UI
- Remplacer les éléments HTML par les composants UI
## 🎨 Avantages
1. **Consistance** - Même apparence partout
2. **Maintenance** - Changements centralisés
3. **Réutilisabilité** - Composants réutilisables
4. **Type Safety** - Props typées
5. **Performance** - Styles optimisés
## 📝 Règles
1. **JAMAIS** de variables CSS dans les composants métier
2. **TOUJOURS** utiliser les composants UI
3. **CRÉER** de nouveaux composants UI si nécessaire
4. **DOCUMENTER** les nouveaux composants UI

View File

@@ -1,158 +0,0 @@
import { httpClient } from './base/http-client';
import { DailyCheckbox, DailyView, Task } from '@/lib/types';
// Types pour les réponses API (avec dates en string)
interface ApiCheckbox {
id: string;
date: string;
text: string;
isChecked: boolean;
type: 'task' | 'meeting';
order: number;
taskId?: string;
task?: Task;
createdAt: string;
updatedAt: string;
}
interface ApiDailyView {
date: string;
yesterday: ApiCheckbox[];
today: ApiCheckbox[];
}
interface ApiHistoryItem {
date: string;
checkboxes: ApiCheckbox[];
}
export interface DailyHistoryFilters {
limit?: number;
}
export interface DailySearchFilters {
query: string;
limit?: number;
}
// Types conservés pour la compatibilité des hooks d'historique et de recherche
export interface ReorderCheckboxesData {
date: Date;
checkboxIds: string[];
}
/**
* Client HTTP pour les données Daily (lecture seule)
* Les mutations sont gérées par les server actions dans actions/daily.ts
*/
export class DailyClient {
/**
* Récupère la vue daily d'aujourd'hui (hier + aujourd'hui)
*/
async getTodaysDailyView(): Promise<DailyView> {
const result = await httpClient.get<ApiDailyView>('/daily');
return this.transformDailyViewDates(result);
}
/**
* Récupère la vue daily pour une date donnée
*/
async getDailyView(date: Date): Promise<DailyView> {
const dateStr = this.formatDateForAPI(date);
const result = await httpClient.get<ApiDailyView>(`/daily?date=${dateStr}`);
return this.transformDailyViewDates(result);
}
/**
* Récupère l'historique des checkboxes
*/
async getCheckboxHistory(filters?: DailyHistoryFilters): Promise<{ date: Date; checkboxes: DailyCheckbox[] }[]> {
const params = new URLSearchParams({ action: 'history' });
if (filters?.limit) params.append('limit', filters.limit.toString());
const result = await httpClient.get<ApiHistoryItem[]>(`/daily?${params}`);
return result.map(item => ({
date: new Date(item.date),
checkboxes: item.checkboxes.map((cb: ApiCheckbox) => this.transformCheckboxDates(cb))
}));
}
/**
* Recherche dans les checkboxes
*/
async searchCheckboxes(filters: DailySearchFilters): Promise<DailyCheckbox[]> {
const params = new URLSearchParams({
action: 'search',
q: filters.query
});
if (filters.limit) params.append('limit', filters.limit.toString());
const result = await httpClient.get<ApiCheckbox[]>(`/daily?${params}`);
return result.map((cb: ApiCheckbox) => this.transformCheckboxDates(cb));
}
/**
* Formate une date pour l'API (évite les décalages timezone)
*/
formatDateForAPI(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`; // YYYY-MM-DD
}
/**
* Transforme les dates string d'une checkbox en objets Date
*/
private transformCheckboxDates(checkbox: ApiCheckbox): DailyCheckbox {
return {
...checkbox,
date: new Date(checkbox.date),
createdAt: new Date(checkbox.createdAt),
updatedAt: new Date(checkbox.updatedAt)
};
}
/**
* Transforme les dates string d'une vue daily en objets Date
*/
private transformDailyViewDates(view: ApiDailyView): DailyView {
return {
date: new Date(view.date),
yesterday: view.yesterday.map((cb: ApiCheckbox) => this.transformCheckboxDates(cb)),
today: view.today.map((cb: ApiCheckbox) => this.transformCheckboxDates(cb))
};
}
/**
* Récupère la vue daily d'une date relative (hier, aujourd'hui, demain)
*/
async getDailyViewByRelativeDate(relative: 'yesterday' | 'today' | 'tomorrow'): Promise<DailyView> {
const date = new Date();
switch (relative) {
case 'yesterday':
date.setDate(date.getDate() - 1);
break;
case 'tomorrow':
date.setDate(date.getDate() + 1);
break;
// 'today' ne change rien
}
return this.getDailyView(date);
}
/**
* Récupère toutes les dates qui ont des dailies
*/
async getDailyDates(): Promise<string[]> {
const response = await httpClient.get<{ dates: string[] }>('/daily/dates');
return response.dates;
}
}
// Instance singleton du client
export const dailyClient = new DailyClient();

View File

@@ -1,36 +0,0 @@
/**
* Client pour l'API Jira
*/
import { HttpClient } from './base/http-client';
import { JiraSyncResult } from '@/services/jira';
export interface JiraConnectionStatus {
connected: boolean;
message: string;
details?: string;
}
export class JiraClient extends HttpClient {
constructor() {
super('/api/jira');
}
/**
* Teste la connexion à Jira
*/
async testConnection(): Promise<JiraConnectionStatus> {
return this.get<JiraConnectionStatus>('/sync');
}
/**
* Lance la synchronisation manuelle des tickets Jira
*/
async syncTasks(): Promise<JiraSyncResult> {
const response = await this.post<{ data: JiraSyncResult }>('/sync');
return response.data;
}
}
// Instance singleton
export const jiraClient = new JiraClient();

View File

@@ -1,28 +0,0 @@
import { httpClient } from './base/http-client';
import { UserPreferences } from '@/lib/types';
export interface UserPreferencesResponse {
success: boolean;
data?: UserPreferences;
message?: string;
error?: string;
}
/**
* Client HTTP pour les préférences utilisateur (lecture seule)
* Les mutations sont gérées par les server actions dans actions/preferences.ts
*/
export const userPreferencesClient = {
/**
* Récupère toutes les préférences utilisateur
*/
async getPreferences(): Promise<UserPreferences> {
const response = await httpClient.get<UserPreferencesResponse>('/user-preferences');
if (!response.success || !response.data) {
throw new Error(response.error || 'Erreur lors de la récupération des préférences');
}
return response.data;
}
};

View File

@@ -1,66 +0,0 @@
'use client';
import { Header } from '@/components/ui/Header';
import { TasksProvider, useTasksContext } from '@/contexts/TasksContext';
import { UserPreferencesProvider } from '@/contexts/UserPreferencesContext';
import { Task, Tag, UserPreferences, TaskStats } from '@/lib/types';
import { CreateTaskData } from '@/clients/tasks-client';
import { DashboardStats } from '@/components/dashboard/DashboardStats';
import { QuickActions } from '@/components/dashboard/QuickActions';
import { RecentTasks } from '@/components/dashboard/RecentTasks';
import { ProductivityAnalytics } from '@/components/dashboard/ProductivityAnalytics';
interface HomePageClientProps {
initialTasks: Task[];
initialTags: (Tag & { usage: number })[];
initialPreferences: UserPreferences;
initialStats: TaskStats;
}
function HomePageContent() {
const { stats, syncing, createTask, tasks } = useTasksContext();
// Handler pour la création de tâche
const handleCreateTask = async (data: CreateTaskData) => {
await createTask(data);
};
return (
<div className="min-h-screen bg-[var(--background)]">
<Header
title="TowerControl"
subtitle="Dashboard - Vue d'ensemble"
syncing={syncing}
/>
<main className="container mx-auto px-6 py-8">
{/* Statistiques */}
<DashboardStats stats={stats} />
{/* Actions rapides */}
<QuickActions onCreateTask={handleCreateTask} />
{/* Analytics et métriques */}
<ProductivityAnalytics />
{/* Tâches récentes */}
<RecentTasks tasks={tasks} />
</main>
</div>
);
}
export function HomePageClient({ initialTasks, initialTags, initialPreferences, initialStats }: HomePageClientProps) {
return (
<UserPreferencesProvider initialPreferences={initialPreferences}>
<TasksProvider
initialTasks={initialTasks}
initialTags={initialTags}
initialStats={initialStats}
>
<HomePageContent />
</TasksProvider>
</UserPreferencesProvider>
);
}

View File

@@ -1,106 +0,0 @@
'use client';
import { useState, useRef } from 'react';
import { DailyCheckboxType } from '@/lib/types';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
interface DailyAddFormProps {
onAdd: (text: string, type: DailyCheckboxType) => Promise<void>;
disabled?: boolean;
placeholder?: string;
}
export function DailyAddForm({ onAdd, disabled = false, placeholder = "Ajouter une tâche..." }: DailyAddFormProps) {
const [newCheckboxText, setNewCheckboxText] = useState('');
const [selectedType, setSelectedType] = useState<DailyCheckboxType>('meeting');
const inputRef = useRef<HTMLInputElement>(null);
const handleAddCheckbox = () => {
if (!newCheckboxText.trim()) return;
const text = newCheckboxText.trim();
// Vider et refocus IMMÉDIATEMENT pour l'UX optimiste
setNewCheckboxText('');
inputRef.current?.focus();
// Lancer l'ajout en arrière-plan (fire and forget)
onAdd(text, selectedType).catch(error => {
console.error('Erreur lors de l\'ajout:', error);
// En cas d'erreur, on pourrait restaurer le texte
// setNewCheckboxText(text);
});
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddCheckbox();
}
};
const getPlaceholder = () => {
if (placeholder !== "Ajouter une tâche...") return placeholder;
return selectedType === 'meeting' ? 'Ajouter une réunion...' : 'Ajouter une tâche...';
};
return (
<div className="space-y-2">
{/* Sélecteur de type */}
<div className="flex gap-2">
<Button
type="button"
onClick={() => setSelectedType('task')}
variant="ghost"
size="sm"
className={`flex items-center gap-1 text-xs border-l-4 ${
selectedType === 'task'
? 'border-l-green-500 bg-green-500/30 text-white font-medium'
: 'border-l-green-300 hover:border-l-green-400 opacity-70 hover:opacity-90'
}`}
disabled={disabled}
>
Tâche
</Button>
<Button
type="button"
onClick={() => setSelectedType('meeting')}
variant="ghost"
size="sm"
className={`flex items-center gap-1 text-xs border-l-4 ${
selectedType === 'meeting'
? 'border-l-blue-500 bg-blue-500/30 text-white font-medium'
: 'border-l-blue-300 hover:border-l-blue-400 opacity-70 hover:opacity-90'
}`}
disabled={disabled}
>
🗓 Réunion
</Button>
</div>
{/* Champ de saisie et options */}
<div className="flex gap-2">
<Input
ref={inputRef}
type="text"
placeholder={getPlaceholder()}
value={newCheckboxText}
onChange={(e) => setNewCheckboxText(e.target.value)}
onKeyDown={handleKeyPress}
disabled={disabled}
className="flex-1 min-w-[300px]"
/>
<Button
onClick={handleAddCheckbox}
disabled={!newCheckboxText.trim() || disabled}
variant="primary"
size="sm"
className="min-w-[40px]"
>
+
</Button>
</div>
</div>
);
}

View File

@@ -1,266 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
import { DailyCheckbox, DailyCheckboxType, Task } from '@/lib/types';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { Modal } from '@/components/ui/Modal';
import { tasksClient } from '@/clients/tasks-client';
interface EditCheckboxModalProps {
checkbox: DailyCheckbox;
isOpen: boolean;
onClose: () => void;
onSave: (text: string, type: DailyCheckboxType, taskId?: string) => Promise<void>;
saving?: boolean;
}
export function EditCheckboxModal({
checkbox,
isOpen,
onClose,
onSave,
saving = false
}: EditCheckboxModalProps) {
const [text, setText] = useState(checkbox.text);
const [type, setType] = useState<DailyCheckboxType>(checkbox.type);
const [taskId, setTaskId] = useState<string | undefined>(checkbox.taskId);
const [selectedTask, setSelectedTask] = useState<Task | undefined>(undefined);
const [allTasks, setAllTasks] = useState<Task[]>([]);
const [tasksLoading, setTasksLoading] = useState(false);
const [taskSearch, setTaskSearch] = useState('');
// Charger toutes les tâches au début
useEffect(() => {
if (isOpen) {
setTasksLoading(true);
tasksClient.getTasks()
.then(response => {
setAllTasks(response.data);
// Trouver la tâche sélectionnée si elle existe
if (taskId) {
const task = response.data.find((t: Task) => t.id === taskId);
setSelectedTask(task);
}
})
.catch(console.error)
.finally(() => setTasksLoading(false));
}
}, [isOpen, taskId]);
// Mettre à jour la tâche sélectionnée quand taskId change
useEffect(() => {
if (taskId && allTasks.length > 0) {
const task = allTasks.find((t: Task) => t.id === taskId);
setSelectedTask(task);
} else {
setSelectedTask(undefined);
}
}, [taskId, allTasks]);
// Filtrer les tâches selon la recherche
const filteredTasks = allTasks.filter(task =>
task.title.toLowerCase().includes(taskSearch.toLowerCase()) ||
(task.description && task.description.toLowerCase().includes(taskSearch.toLowerCase()))
);
const handleTaskSelect = (task: Task) => {
setTaskId(task.id);
setTaskSearch(''); // Fermer la recherche après sélection
};
const handleSave = async () => {
if (!text.trim()) return;
try {
await onSave(text.trim(), type, taskId);
onClose();
} catch (error) {
console.error('Erreur lors de la sauvegarde:', error);
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSave();
}
};
const resetForm = () => {
setText(checkbox.text);
setType(checkbox.type);
setTaskId(checkbox.taskId);
};
const handleClose = () => {
resetForm();
onClose();
};
return (
<Modal isOpen={isOpen} onClose={handleClose} title="Modifier la tâche">
<div className="space-y-4">
{/* Texte */}
<div>
<label className="block text-sm font-medium text-[var(--foreground)] mb-2">
Description
</label>
<Input
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={handleKeyPress}
placeholder="Description de la tâche..."
className="w-full"
autoFocus
/>
</div>
{/* Type */}
<div>
<label className="block text-sm font-medium text-[var(--foreground)] mb-2">
Type
</label>
<div className="flex gap-2">
<Button
type="button"
onClick={() => setType('task')}
variant="ghost"
size="sm"
className={`flex items-center gap-2 border-l-4 ${
type === 'task'
? 'border-l-green-500 bg-green-500/30 text-white font-medium'
: 'border-l-green-300 hover:border-l-green-400 opacity-70 hover:opacity-90'
}`}
>
Tâche
</Button>
<Button
type="button"
onClick={() => setType('meeting')}
variant="ghost"
size="sm"
className={`flex items-center gap-2 border-l-4 ${
type === 'meeting'
? 'border-l-blue-500 bg-blue-500/30 text-white font-medium'
: 'border-l-blue-300 hover:border-l-blue-400 opacity-70 hover:opacity-90'
}`}
>
🗓 Réunion
</Button>
</div>
</div>
{/* Liaison tâche (pour tous les types) */}
<div>
<label className="block text-sm font-medium text-[var(--foreground)] mb-2">
Lier à une tâche (optionnel)
</label>
{selectedTask ? (
// Tâche déjà sélectionnée
<div className="border border-[var(--border)] rounded-lg p-3 bg-[var(--muted)]/30">
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="font-medium text-sm">{selectedTask.title}</div>
{selectedTask.description && (
<div className="text-xs text-[var(--muted-foreground)] truncate">
{selectedTask.description}
</div>
)}
<span className={`inline-block px-1 py-0.5 rounded text-xs mt-1 ${
selectedTask.status === 'todo' ? 'bg-blue-100 text-blue-800' :
selectedTask.status === 'in_progress' ? 'bg-yellow-100 text-yellow-800' :
'bg-gray-100 text-gray-800'
}`}>
{selectedTask.status}
</span>
</div>
<Button
type="button"
onClick={() => setTaskId(undefined)}
variant="ghost"
size="sm"
className="text-[var(--destructive)] hover:bg-[var(--destructive)]/10"
disabled={saving}
>
×
</Button>
</div>
</div>
) : (
// Interface de sélection simplifiée
<div className="space-y-2">
<Input
type="text"
placeholder="Rechercher une tâche..."
value={taskSearch}
onChange={(e) => setTaskSearch(e.target.value)}
disabled={saving || tasksLoading}
className="w-full"
/>
{taskSearch.trim() && (
<div className="border border-[var(--border)] rounded-lg max-h-40 overflow-y-auto">
{tasksLoading ? (
<div className="p-3 text-center text-sm text-[var(--muted-foreground)]">
Chargement...
</div>
) : filteredTasks.length === 0 ? (
<div className="p-3 text-center text-sm text-[var(--muted-foreground)]">
Aucune tâche trouvée
</div>
) : (
filteredTasks.slice(0, 5).map((task) => (
<button
key={task.id}
type="button"
onClick={() => handleTaskSelect(task)}
className="w-full text-left p-3 hover:bg-[var(--muted)]/50 transition-colors border-b border-[var(--border)]/30 last:border-b-0"
disabled={saving}
>
<div className="font-medium text-sm">{task.title}</div>
{task.description && (
<div className="text-xs text-[var(--muted-foreground)] truncate mt-1">
{task.description}
</div>
)}
<span className={`inline-block px-1 py-0.5 rounded text-xs mt-1 ${
task.status === 'todo' ? 'bg-blue-100 text-blue-800' :
task.status === 'in_progress' ? 'bg-yellow-100 text-yellow-800' :
'bg-gray-100 text-gray-800'
}`}>
{task.status}
</span>
</button>
))
)}
</div>
)}
</div>
)}
</div>
{/* Actions */}
<div className="flex gap-2 justify-end pt-4">
<Button
type="button"
onClick={handleClose}
variant="ghost"
disabled={saving}
>
Annuler
</Button>
<Button
type="button"
onClick={handleSave}
variant="primary"
disabled={!text.trim() || saving}
>
{saving ? 'Sauvegarde...' : 'Sauvegarder'}
</Button>
</div>
</div>
</Modal>
);
}

View File

@@ -1,146 +0,0 @@
'use client';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
interface CategoryData {
count: number;
percentage: number;
color: string;
icon: string;
}
interface CategoryBreakdownProps {
categoryData: { [categoryName: string]: CategoryData };
totalActivities: number;
}
export function CategoryBreakdown({ categoryData, totalActivities }: CategoryBreakdownProps) {
const categories = Object.entries(categoryData)
.filter(([, data]) => data.count > 0)
.sort((a, b) => b[1].count - a[1].count);
if (categories.length === 0) {
return (
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">📊 Répartition par catégorie</h3>
</CardHeader>
<CardContent>
<p className="text-center text-[var(--muted-foreground)]">
Aucune activité à catégoriser
</p>
</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">📊 Répartition par catégorie</h3>
<p className="text-sm text-[var(--muted-foreground)]">
Analyse automatique de vos {totalActivities} activités
</p>
</CardHeader>
<CardContent className="space-y-6">
{/* Légende des catégories */}
<div className="flex flex-wrap gap-3 justify-center">
{categories.map(([categoryName, data]) => (
<div
key={categoryName}
className="flex items-center gap-2 bg-[var(--card)] border border-[var(--border)] rounded-lg px-3 py-2 hover:border-[var(--primary)]/50 transition-colors"
>
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: data.color }}
/>
<span className="text-sm font-medium text-[var(--foreground)]">
{data.icon} {categoryName}
</span>
<Badge className="bg-[var(--primary)]/10 text-[var(--primary)] text-xs">
{data.count}
</Badge>
</div>
))}
</div>
{/* Barres de progression */}
<div className="space-y-3">
{categories.map(([categoryName, data]) => (
<div key={categoryName} className="space-y-1">
<div className="flex items-center justify-between text-sm">
<span className="flex items-center gap-2">
<span>{data.icon}</span>
<span className="font-medium">{categoryName}</span>
</span>
<span className="text-[var(--muted-foreground)]">
{data.count} ({data.percentage.toFixed(1)}%)
</span>
</div>
<div className="w-full bg-[var(--border)] rounded-full h-2">
<div
className="h-2 rounded-full transition-all duration-500"
style={{
backgroundColor: data.color,
width: `${data.percentage}%`
}}
/>
</div>
</div>
))}
</div>
{/* Insights */}
<div className="bg-[var(--card)] p-4 rounded-lg border border-[var(--border)]">
<h4 className="font-medium mb-2">💡 Insights</h4>
<div className="text-sm text-[var(--muted-foreground)] space-y-1">
{categories.length > 0 && (
<>
<p>
🏆 <strong>{categories[0][0]}</strong> est votre activité principale
({categories[0][1].percentage.toFixed(1)}% de votre temps).
</p>
{categories.length > 1 && (
<p>
📈 Vous avez une bonne diversité avec {categories.length} catégories d&apos;activités.
</p>
)}
{/* Suggestions basées sur la répartition */}
{categories.some(([, data]) => data.percentage > 70) && (
<p>
Forte concentration sur une seule catégorie.
Pensez à diversifier vos activités pour un meilleur équilibre.
</p>
)}
{(() => {
const learningCategory = categories.find(([name]) => name === 'Learning');
return learningCategory && learningCategory[1].percentage > 0 && (
<p>
🎓 Excellent ! Vous consacrez du temps à l&apos;apprentissage
({learningCategory[1].percentage.toFixed(1)}%).
</p>
);
})()}
{(() => {
const devCategory = categories.find(([name]) => name === 'Dev');
return devCategory && devCategory[1].percentage > 50 && (
<p>
💻 Focus développement intense. N&apos;oubliez pas les pauses et la collaboration !
</p>
);
})()}
</>
)}
</div>
</div>
</CardContent>
</Card>
);
}

View File

@@ -1,128 +0,0 @@
'use client';
import { TaskStats } from '@/lib/types';
import { Card } from '@/components/ui/Card';
import { getDashboardStatColors } from '@/lib/status-config';
interface DashboardStatsProps {
stats: TaskStats;
}
export function DashboardStats({ stats }: DashboardStatsProps) {
const totalTasks = stats.total;
const completionRate = totalTasks > 0 ? Math.round((stats.completed / totalTasks) * 100) : 0;
const inProgressRate = totalTasks > 0 ? Math.round((stats.inProgress / totalTasks) * 100) : 0;
const statCards = [
{
title: 'Total Tâches',
value: stats.total,
icon: '📋',
type: 'total' as const,
...getDashboardStatColors('total')
},
{
title: 'À Faire',
value: stats.todo,
icon: '⏳',
type: 'todo' as const,
...getDashboardStatColors('todo')
},
{
title: 'En Cours',
value: stats.inProgress,
icon: '🔄',
type: 'inProgress' as const,
...getDashboardStatColors('inProgress')
},
{
title: 'Terminées',
value: stats.completed,
icon: '✅',
type: 'completed' as const,
...getDashboardStatColors('completed')
}
];
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{statCards.map((stat, index) => (
<Card key={index} className="p-6 hover:shadow-lg transition-shadow">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--muted-foreground)] mb-1">
{stat.title}
</p>
<p className={`text-3xl font-bold ${stat.textColor}`}>
{stat.value}
</p>
</div>
<div className="text-3xl">
{stat.icon}
</div>
</div>
</Card>
))}
{/* Cartes de pourcentage */}
<Card className="p-6 hover:shadow-lg transition-shadow md:col-span-2 lg:col-span-2">
<h3 className="text-lg font-semibold mb-4">Taux de Completion</h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Terminées</span>
<span className={`font-bold ${getDashboardStatColors('completed').textColor}`}>{completionRate}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${getDashboardStatColors('completed').progressColor}`}
style={{ width: `${completionRate}%` }}
/>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium">En Cours</span>
<span className={`font-bold ${getDashboardStatColors('inProgress').textColor}`}>{inProgressRate}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${getDashboardStatColors('inProgress').progressColor}`}
style={{ width: `${inProgressRate}%` }}
/>
</div>
</div>
</Card>
{/* Insights rapides */}
<Card className="p-6 hover:shadow-lg transition-shadow md:col-span-2 lg:col-span-2">
<h3 className="text-lg font-semibold mb-4">Aperçu Rapide</h3>
<div className="space-y-3">
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${getDashboardStatColors('completed').dotColor}`}></span>
<span className="text-sm">
{stats.completed} tâches terminées sur {totalTasks}
</span>
</div>
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${getDashboardStatColors('inProgress').dotColor}`}></span>
<span className="text-sm">
{stats.inProgress} tâches en cours de traitement
</span>
</div>
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${getDashboardStatColors('todo').dotColor}`}></span>
<span className="text-sm">
{stats.todo} tâches en attente
</span>
</div>
{totalTasks > 0 && (
<div className="pt-2 border-t border-[var(--border)]">
<span className="text-sm font-medium text-[var(--muted-foreground)]">
Productivité: {completionRate}% de completion
</span>
</div>
)}
</div>
</Card>
</div>
);
}

View File

@@ -1,193 +0,0 @@
'use client';
import type { JiraWeeklyMetrics } from '@/services/jira-summary';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import { JiraSummaryService } from '@/services/jira-summary';
interface JiraWeeklyMetricsProps {
jiraMetrics: JiraWeeklyMetrics | null;
}
export function JiraWeeklyMetrics({ jiraMetrics }: JiraWeeklyMetricsProps) {
if (!jiraMetrics) {
return (
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">🔗 Contexte business Jira</h3>
</CardHeader>
<CardContent>
<p className="text-center text-[var(--muted-foreground)]">
Configuration Jira non disponible
</p>
</CardContent>
</Card>
);
}
if (jiraMetrics.totalJiraTasks === 0) {
return (
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">🔗 Contexte business Jira</h3>
</CardHeader>
<CardContent>
<p className="text-center text-[var(--muted-foreground)]">
Aucune tâche Jira cette semaine
</p>
</CardContent>
</Card>
);
}
const completionRate = (jiraMetrics.completedJiraTasks / jiraMetrics.totalJiraTasks) * 100;
const insights = JiraSummaryService.generateBusinessInsights(jiraMetrics);
return (
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">🔗 Contexte business Jira</h3>
<p className="text-sm text-[var(--muted-foreground)]">
Impact business et métriques projet
</p>
</CardHeader>
<CardContent className="space-y-6">
{/* Métriques principales */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div className="bg-[var(--card)] p-4 rounded-lg border border-[var(--border)] hover:border-[var(--primary)]/50 transition-colors text-center">
<div className="text-2xl font-bold text-[var(--primary)]">
{jiraMetrics.totalJiraTasks}
</div>
<div className="text-sm text-[var(--muted-foreground)]">Tickets Jira</div>
</div>
<div className="bg-[var(--card)] p-4 rounded-lg border border-[var(--border)] hover:border-[var(--success)]/50 transition-colors text-center">
<div className="text-2xl font-bold text-[var(--success)]">
{completionRate.toFixed(0)}%
</div>
<div className="text-sm text-[var(--muted-foreground)]">Taux completion</div>
</div>
<div className="bg-[var(--card)] p-4 rounded-lg border border-[var(--border)] hover:border-[var(--accent)]/50 transition-colors text-center">
<div className="text-2xl font-bold text-[var(--accent)]">
{jiraMetrics.totalStoryPoints}
</div>
<div className="text-sm text-[var(--muted-foreground)]">Story Points*</div>
</div>
<div className="bg-[var(--card)] p-4 rounded-lg border border-[var(--border)] hover:border-[var(--warning)]/50 transition-colors text-center">
<div className="text-2xl font-bold text-[var(--warning)]">
{jiraMetrics.projectsContributed.length}
</div>
<div className="text-sm text-[var(--muted-foreground)]">Projet(s)</div>
</div>
</div>
{/* Projets contributés */}
{jiraMetrics.projectsContributed.length > 0 && (
<div>
<h4 className="font-medium mb-2">📂 Projets contributés</h4>
<div className="flex flex-wrap gap-2">
{jiraMetrics.projectsContributed.map(project => (
<Badge key={project} className="bg-[var(--primary)]/10 text-[var(--primary)]">
{project}
</Badge>
))}
</div>
</div>
)}
{/* Types de tickets */}
<div>
<h4 className="font-medium mb-3">🎯 Types de tickets</h4>
<div className="space-y-2">
{Object.entries(jiraMetrics.ticketTypes)
.sort(([,a], [,b]) => b - a)
.map(([type, count]) => {
const percentage = (count / jiraMetrics.totalJiraTasks) * 100;
return (
<div key={type} className="flex items-center justify-between">
<span className="text-sm text-[var(--foreground)]">{type}</span>
<div className="flex items-center gap-2">
<div className="w-20 bg-[var(--border)] rounded-full h-2">
<div
className="h-2 bg-[var(--primary)] rounded-full transition-all"
style={{ width: `${percentage}%` }}
/>
</div>
<span className="text-sm text-[var(--muted-foreground)] w-8">
{count}
</span>
</div>
</div>
);
})}
</div>
</div>
{/* Liens vers les tickets */}
<div>
<h4 className="font-medium mb-3">🎫 Tickets traités</h4>
<div className="space-y-2 max-h-40 overflow-y-auto">
{jiraMetrics.jiraLinks.map((link) => (
<div
key={link.key}
className="flex items-center justify-between p-2 rounded border hover:bg-[var(--muted)] transition-colors"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="text-[var(--primary)] hover:underline font-medium text-sm"
>
{link.key}
</a>
<Badge
className={`text-xs ${
link.status === 'done'
? 'bg-[var(--success)]/10 text-[var(--success)]'
: 'bg-[var(--muted)]/50 text-[var(--muted-foreground)]'
}`}
>
{link.status}
</Badge>
</div>
<p className="text-xs text-[var(--muted-foreground)] truncate">
{link.title}
</p>
</div>
<div className="flex items-center gap-2 text-xs text-[var(--muted-foreground)]">
<span>{link.type}</span>
<span>{link.estimatedPoints}pts</span>
</div>
</div>
))}
</div>
</div>
{/* Insights business */}
{insights.length > 0 && (
<div className="bg-[var(--card)] p-4 rounded-lg border border-[var(--border)]">
<h4 className="font-medium mb-2">💡 Insights business</h4>
<div className="text-sm text-[var(--muted-foreground)] space-y-1">
{insights.map((insight, index) => (
<p key={index}>{insight}</p>
))}
</div>
</div>
)}
{/* Note sur les story points */}
<div className="text-xs text-[var(--muted-foreground)] bg-[var(--card)] border border-[var(--border)] p-2 rounded">
<p>
* Story Points estimés automatiquement basés sur le type de ticket
(Epic: 8pts, Story: 3pts, Task: 2pts, Bug: 1pt)
</p>
</div>
</CardContent>
</Card>
);
}

View File

@@ -1,497 +0,0 @@
'use client';
import { useState } from 'react';
import { ManagerSummary } from '@/services/manager-summary';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { TagDisplay } from '@/components/ui/TagDisplay';
import { getPriorityConfig } from '@/lib/status-config';
import { useTasksContext } from '@/contexts/TasksContext';
import { MetricsTab } from './MetricsTab';
import { format } from 'date-fns';
import { fr } from 'date-fns/locale';
interface ManagerWeeklySummaryProps {
initialSummary: ManagerSummary;
}
export default function ManagerWeeklySummary({ initialSummary }: ManagerWeeklySummaryProps) {
const [summary] = useState<ManagerSummary>(initialSummary);
const [activeView, setActiveView] = useState<'narrative' | 'accomplishments' | 'challenges' | 'metrics'>('narrative');
const { tags: availableTags } = useTasksContext();
const handleRefresh = () => {
// SSR - refresh via page reload
window.location.reload();
};
const formatPeriod = () => {
return `Semaine du ${format(summary.period.start, 'dd MMM', { locale: fr })} au ${format(summary.period.end, 'dd MMM yyyy', { locale: fr })}`;
};
const getPriorityBadgeStyle = (priority: 'low' | 'medium' | 'high') => {
const config = getPriorityConfig(priority);
const baseClasses = 'text-xs px-2 py-0.5 rounded font-medium';
switch (config.color) {
case 'blue':
return `${baseClasses} bg-blue-100 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400`;
case 'yellow':
return `${baseClasses} bg-yellow-100 dark:bg-yellow-900/20 text-yellow-600 dark:text-yellow-400`;
case 'purple':
return `${baseClasses} bg-purple-100 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400`;
case 'red':
return `${baseClasses} bg-red-100 dark:bg-red-900/20 text-red-600 dark:text-red-400`;
default:
return `${baseClasses} bg-gray-100 dark:bg-gray-900/20 text-gray-600 dark:text-gray-400`;
}
};
return (
<div className="space-y-6">
{/* Header avec navigation */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-[var(--foreground)]">👔 Résumé Manager</h1>
<p className="text-[var(--muted-foreground)]">{formatPeriod()}</p>
</div>
<Button
onClick={handleRefresh}
variant="secondary"
size="sm"
>
🔄 Actualiser
</Button>
</div>
{/* Navigation des vues */}
<div className="border-b border-[var(--border)]">
<nav className="flex space-x-8">
<button
onClick={() => setActiveView('narrative')}
className={`py-3 px-1 border-b-2 font-medium text-sm transition-colors ${
activeView === 'narrative'
? 'border-[var(--primary)] text-[var(--primary)]'
: 'border-transparent text-[var(--muted-foreground)] hover:text-[var(--foreground)]'
}`}
>
📝 Vue Executive
</button>
<button
onClick={() => setActiveView('accomplishments')}
className={`py-3 px-1 border-b-2 font-medium text-sm transition-colors ${
activeView === 'accomplishments'
? 'border-[var(--primary)] text-[var(--primary)]'
: 'border-transparent text-[var(--muted-foreground)] hover:text-[var(--foreground)]'
}`}
>
Accomplissements ({summary.keyAccomplishments.length})
</button>
<button
onClick={() => setActiveView('challenges')}
className={`py-3 px-1 border-b-2 font-medium text-sm transition-colors ${
activeView === 'challenges'
? 'border-[var(--primary)] text-[var(--primary)]'
: 'border-transparent text-[var(--muted-foreground)] hover:text-[var(--foreground)]'
}`}
>
🎯 Enjeux à venir ({summary.upcomingChallenges.length})
</button>
<button
onClick={() => setActiveView('metrics')}
className={`py-3 px-1 border-b-2 font-medium text-sm transition-colors ${
activeView === 'metrics'
? 'border-[var(--primary)] text-[var(--primary)]'
: 'border-transparent text-[var(--muted-foreground)] hover:text-[var(--foreground)]'
}`}
>
📊 Métriques
</button>
</nav>
</div>
{/* Vue Executive / Narrative */}
{activeView === 'narrative' && (
<div className="space-y-6">
{/* Résumé narratif */}
<Card>
<CardHeader>
<h2 className="text-lg font-semibold flex items-center gap-2">
📊 Résumé de la semaine
</h2>
</CardHeader>
<CardContent className="space-y-4">
<div className="bg-blue-50 p-4 rounded-lg border-l-4 border-blue-400">
<h3 className="font-medium text-blue-900 mb-2">🎯 Points clés accomplis</h3>
<p className="text-blue-800">{summary.narrative.weekHighlight}</p>
</div>
<div className="bg-yellow-50 p-4 rounded-lg border-l-4 border-yellow-400">
<h3 className="font-medium text-yellow-900 mb-2"> Défis traités</h3>
<p className="text-yellow-800">{summary.narrative.mainChallenges}</p>
</div>
<div className="bg-green-50 p-4 rounded-lg border-l-4 border-green-400">
<h3 className="font-medium text-green-900 mb-2">🔮 Focus semaine prochaine</h3>
<p className="text-green-800">{summary.narrative.nextWeekFocus}</p>
</div>
</CardContent>
</Card>
{/* Métriques rapides */}
<Card>
<CardHeader>
<h2 className="text-lg font-semibold">📈 Métriques en bref</h2>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div className="text-center p-4 bg-blue-50 rounded-lg">
<div className="text-2xl font-bold text-blue-600">
{summary.metrics.totalTasksCompleted}
</div>
<div className="text-sm text-blue-600">Tâches complétées</div>
<div className="text-xs text-blue-500">
dont {summary.metrics.highPriorityTasksCompleted} priorité haute
</div>
</div>
<div className="text-center p-4 bg-green-50 rounded-lg">
<div className="text-2xl font-bold text-green-600">
{summary.metrics.totalCheckboxesCompleted}
</div>
<div className="text-sm text-green-600">Todos complétés</div>
<div className="text-xs text-green-500">
dont {summary.metrics.meetingCheckboxesCompleted} meetings
</div>
</div>
<div className="text-center p-4 bg-purple-50 rounded-lg">
<div className="text-2xl font-bold text-purple-600">
{summary.keyAccomplishments.filter(a => a.impact === 'high').length}
</div>
<div className="text-sm text-purple-600">Items à fort impact</div>
<div className="text-xs text-purple-500">
/ {summary.keyAccomplishments.length} accomplissements
</div>
</div>
<div className="text-center p-4 bg-orange-50 rounded-lg">
<div className="text-2xl font-bold text-orange-600">
{summary.upcomingChallenges.filter(c => c.priority === 'high').length}
</div>
<div className="text-sm text-orange-600">Priorités critiques</div>
<div className="text-xs text-orange-500">
/ {summary.upcomingChallenges.length} enjeux
</div>
</div>
</div>
</CardContent>
</Card>
{/* Top accomplissements */}
<Card>
<CardHeader>
<h2 className="text-lg font-semibold">🏆 Top accomplissements</h2>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{summary.keyAccomplishments.length === 0 ? (
<div className="col-span-3 text-center py-8 text-[var(--muted-foreground)]">
<p>Aucun accomplissement significatif trouvé cette semaine.</p>
<p className="text-sm mt-2">Ajoutez des tâches avec priorité haute/medium ou des meetings.</p>
</div>
) : (
summary.keyAccomplishments.slice(0, 6).map((accomplishment, index) => (
<div
key={accomplishment.id}
className="relative bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 transition-all duration-200 group"
>
{/* Barre colorée gauche */}
<div className="absolute left-0 top-0 bottom-0 w-1 bg-green-500 rounded-l-lg"></div>
{/* Header compact */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="w-5 h-5 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded-full text-xs font-bold flex items-center justify-center">
#{index + 1}
</span>
<span className={getPriorityBadgeStyle(accomplishment.impact)}>
{getPriorityConfig(accomplishment.impact).label}
</span>
</div>
<span className="text-xs text-[var(--muted-foreground)]">
{format(accomplishment.completedAt, 'dd/MM', { locale: fr })}
</span>
</div>
{/* Titre */}
<h4 className="font-semibold text-sm text-[var(--foreground)] mb-2 line-clamp-2">
{accomplishment.title}
</h4>
{/* Tags */}
{accomplishment.tags && accomplishment.tags.length > 0 && (
<div className="mb-2">
<TagDisplay
tags={accomplishment.tags}
availableTags={availableTags}
size="sm"
maxTags={2}
/>
</div>
)}
{/* Description si disponible */}
{accomplishment.description && (
<p className="text-xs text-[var(--muted-foreground)] line-clamp-2 leading-relaxed mb-2">
{accomplishment.description}
</p>
)}
{/* Count de todos */}
{accomplishment.todosCount > 0 && (
<div className="flex items-center gap-1 text-xs text-[var(--muted-foreground)]">
<span>📋</span>
<span>{accomplishment.todosCount} todo{accomplishment.todosCount > 1 ? 's' : ''}</span>
</div>
)}
</div>
))
)}
</div>
</CardContent>
</Card>
{/* Top challenges */}
<Card>
<CardHeader>
<h2 className="text-lg font-semibold">🎯 Top enjeux à venir</h2>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{summary.upcomingChallenges.length === 0 ? (
<div className="col-span-3 text-center py-8 text-[var(--muted-foreground)]">
<p>Aucun enjeu prioritaire trouvé.</p>
<p className="text-sm mt-2">Ajoutez des tâches non complétées avec priorité haute/medium.</p>
</div>
) : (
summary.upcomingChallenges.slice(0, 6).map((challenge, index) => (
<div
key={challenge.id}
className="relative bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 transition-all duration-200 group"
>
{/* Barre colorée gauche */}
<div className="absolute left-0 top-0 bottom-0 w-1 bg-orange-500 rounded-l-lg"></div>
{/* Header compact */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="w-5 h-5 bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300 rounded-full text-xs font-bold flex items-center justify-center">
#{index + 1}
</span>
<span className={getPriorityBadgeStyle(challenge.priority)}>
{getPriorityConfig(challenge.priority).label}
</span>
</div>
{challenge.deadline && (
<span className="text-xs text-[var(--muted-foreground)]">
{format(challenge.deadline, 'dd/MM', { locale: fr })}
</span>
)}
</div>
{/* Titre */}
<h4 className="font-semibold text-sm text-[var(--foreground)] mb-2 line-clamp-2">
{challenge.title}
</h4>
{/* Tags */}
{challenge.tags && challenge.tags.length > 0 && (
<div className="mb-2">
<TagDisplay
tags={challenge.tags}
availableTags={availableTags}
size="sm"
maxTags={2}
/>
</div>
)}
{/* Description si disponible */}
{challenge.description && (
<p className="text-xs text-[var(--muted-foreground)] line-clamp-2 leading-relaxed mb-2">
{challenge.description}
</p>
)}
{/* Count de todos */}
{challenge.todosCount > 0 && (
<div className="flex items-center gap-1 text-xs text-[var(--muted-foreground)]">
<span>📋</span>
<span>{challenge.todosCount} todo{challenge.todosCount > 1 ? 's' : ''}</span>
</div>
)}
</div>
))
)}
</div>
</CardContent>
</Card>
</div>
)}
{/* Vue détaillée des accomplissements */}
{activeView === 'accomplishments' && (
<Card>
<CardHeader>
<h2 className="text-lg font-semibold"> Accomplissements de la semaine</h2>
<p className="text-sm text-[var(--muted-foreground)]">
{summary.keyAccomplishments.length} accomplissements significatifs {summary.metrics.totalTasksCompleted} tâches {summary.metrics.totalCheckboxesCompleted} todos complétés
</p>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{summary.keyAccomplishments.map((accomplishment, index) => (
<div
key={accomplishment.id}
className="relative bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 transition-all duration-200 group"
>
{/* Barre colorée gauche */}
<div className="absolute left-0 top-0 bottom-0 w-1 bg-green-500 rounded-l-lg"></div>
{/* Header compact */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="w-5 h-5 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded-full text-xs font-bold flex items-center justify-center">
#{index + 1}
</span>
<span className={getPriorityBadgeStyle(accomplishment.impact)}>
{getPriorityConfig(accomplishment.impact).label}
</span>
</div>
<span className="text-xs text-[var(--muted-foreground)]">
{format(accomplishment.completedAt, 'dd/MM', { locale: fr })}
</span>
</div>
{/* Titre */}
<h4 className="font-semibold text-sm text-[var(--foreground)] mb-2 line-clamp-2">
{accomplishment.title}
</h4>
{/* Tags */}
{accomplishment.tags && accomplishment.tags.length > 0 && (
<div className="mb-2">
<TagDisplay
tags={accomplishment.tags}
availableTags={availableTags}
size="sm"
maxTags={3}
/>
</div>
)}
{/* Description si disponible */}
{accomplishment.description && (
<p className="text-xs text-[var(--muted-foreground)] line-clamp-3 leading-relaxed mb-2">
{accomplishment.description}
</p>
)}
{/* Count de todos */}
{accomplishment.todosCount > 0 && (
<div className="flex items-center gap-1 text-xs text-[var(--muted-foreground)]">
<span>📋</span>
<span>{accomplishment.todosCount} todo{accomplishment.todosCount > 1 ? 's' : ''}</span>
</div>
)}
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* Vue détaillée des challenges */}
{activeView === 'challenges' && (
<Card>
<CardHeader>
<h2 className="text-lg font-semibold">🎯 Enjeux et défis à venir</h2>
<p className="text-sm text-[var(--muted-foreground)]">
{summary.upcomingChallenges.length} défis identifiés {summary.upcomingChallenges.filter(c => c.priority === 'high').length} priorité haute {summary.upcomingChallenges.filter(c => c.blockers.length > 0).length} avec blockers
</p>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{summary.upcomingChallenges.map((challenge, index) => (
<div
key={challenge.id}
className="relative bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 transition-all duration-200 group"
>
{/* Barre colorée gauche */}
<div className="absolute left-0 top-0 bottom-0 w-1 bg-orange-500 rounded-l-lg"></div>
{/* Header compact */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="w-5 h-5 bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300 rounded-full text-xs font-bold flex items-center justify-center">
#{index + 1}
</span>
<span className={getPriorityBadgeStyle(challenge.priority)}>
{getPriorityConfig(challenge.priority).label}
</span>
</div>
{challenge.deadline && (
<span className="text-xs text-[var(--muted-foreground)]">
{format(challenge.deadline, 'dd/MM', { locale: fr })}
</span>
)}
</div>
{/* Titre */}
<h4 className="font-semibold text-sm text-[var(--foreground)] mb-2 line-clamp-2">
{challenge.title}
</h4>
{/* Tags */}
{challenge.tags && challenge.tags.length > 0 && (
<div className="mb-2">
<TagDisplay
tags={challenge.tags}
availableTags={availableTags}
size="sm"
maxTags={3}
/>
</div>
)}
{/* Description si disponible */}
{challenge.description && (
<p className="text-xs text-[var(--muted-foreground)] line-clamp-3 leading-relaxed mb-2">
{challenge.description}
</p>
)}
{/* Count de todos */}
{challenge.todosCount > 0 && (
<div className="flex items-center gap-1 text-xs text-[var(--muted-foreground)]">
<span>📋</span>
<span>{challenge.todosCount} todo{challenge.todosCount > 1 ? 's' : ''}</span>
</div>
)}
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* Vue Métriques */}
{activeView === 'metrics' && (
<MetricsTab />
)}
</div>
);
}

View File

@@ -1,245 +0,0 @@
'use client';
import { useState } from 'react';
import { useWeeklyMetrics, useVelocityTrends } from '@/hooks/use-metrics';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { DailyStatusChart } from './charts/DailyStatusChart';
import { CompletionRateChart } from './charts/CompletionRateChart';
import { StatusDistributionChart } from './charts/StatusDistributionChart';
import { PriorityBreakdownChart } from './charts/PriorityBreakdownChart';
import { VelocityTrendChart } from './charts/VelocityTrendChart';
import { WeeklyActivityHeatmap } from './charts/WeeklyActivityHeatmap';
import { ProductivityInsights } from './charts/ProductivityInsights';
import { format } from 'date-fns';
import { fr } from 'date-fns/locale';
interface MetricsTabProps {
className?: string;
}
export function MetricsTab({ className }: MetricsTabProps) {
const [selectedDate] = useState<Date>(new Date());
const [weeksBack, setWeeksBack] = useState(4);
const { metrics, loading: metricsLoading, error: metricsError, refetch: refetchMetrics } = useWeeklyMetrics(selectedDate);
const { trends, loading: trendsLoading, error: trendsError, refetch: refetchTrends } = useVelocityTrends(weeksBack);
const handleRefresh = () => {
refetchMetrics();
refetchTrends();
};
const formatPeriod = () => {
if (!metrics) return '';
return `Semaine du ${format(metrics.period.start, 'dd MMM', { locale: fr })} au ${format(metrics.period.end, 'dd MMM yyyy', { locale: fr })}`;
};
const getTrendIcon = (trend: string) => {
switch (trend) {
case 'improving': return '📈';
case 'declining': return '📉';
case 'stable': return '➡️';
default: return '📊';
}
};
const getPatternIcon = (pattern: string) => {
switch (pattern) {
case 'consistent': return '🎯';
case 'variable': return '📊';
case 'weekend-heavy': return '📅';
default: return '📋';
}
};
if (metricsError || trendsError) {
return (
<div className={className}>
<Card>
<CardContent className="p-6 text-center">
<p className="text-red-500 mb-4">
Erreur lors du chargement des métriques
</p>
<p className="text-sm text-[var(--muted-foreground)] mb-4">
{metricsError || trendsError}
</p>
<Button onClick={handleRefresh} variant="secondary" size="sm">
🔄 Réessayer
</Button>
</CardContent>
</Card>
</div>
);
}
return (
<div className={className}>
{/* Header avec période et contrôles */}
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-xl font-bold text-[var(--foreground)]">📊 Métriques & Analytics</h2>
<p className="text-[var(--muted-foreground)]">{formatPeriod()}</p>
</div>
<div className="flex items-center gap-2">
<Button
onClick={handleRefresh}
variant="secondary"
size="sm"
disabled={metricsLoading || trendsLoading}
>
🔄 Actualiser
</Button>
</div>
</div>
{metricsLoading || trendsLoading ? (
<Card>
<CardContent className="p-6 text-center">
<div className="animate-pulse">
<div className="h-4 bg-[var(--border)] rounded w-1/4 mx-auto mb-4"></div>
<div className="h-32 bg-[var(--border)] rounded"></div>
</div>
<p className="text-[var(--muted-foreground)] mt-4">Chargement des métriques...</p>
</CardContent>
</Card>
) : metrics ? (
<div className="space-y-6">
{/* Vue d'ensemble rapide */}
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">🎯 Vue d&apos;ensemble</h3>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4">
<div className="text-center p-4 bg-green-50 dark:bg-green-950/20 rounded-lg">
<div className="text-2xl font-bold text-green-600">
{metrics.summary.totalTasksCompleted}
</div>
<div className="text-sm text-green-600">Terminées</div>
</div>
<div className="text-center p-4 bg-blue-50 dark:bg-blue-950/20 rounded-lg">
<div className="text-2xl font-bold text-blue-600">
{metrics.summary.totalTasksCreated}
</div>
<div className="text-sm text-blue-600">Créées</div>
</div>
<div className="text-center p-4 bg-purple-50 dark:bg-purple-950/20 rounded-lg">
<div className="text-2xl font-bold text-purple-600">
{metrics.summary.averageCompletionRate.toFixed(1)}%
</div>
<div className="text-sm text-purple-600">Taux moyen</div>
</div>
<div className="text-center p-4 bg-orange-50 dark:bg-orange-950/20 rounded-lg">
<div className="text-2xl font-bold text-orange-600">
{getTrendIcon(metrics.summary.trendsAnalysis.completionTrend)}
</div>
<div className="text-sm text-orange-600 capitalize">
{metrics.summary.trendsAnalysis.completionTrend}
</div>
</div>
<div className="text-center p-4 bg-gray-50 dark:bg-gray-950/20 rounded-lg">
<div className="text-2xl font-bold text-gray-600">
{getPatternIcon(metrics.summary.trendsAnalysis.productivityPattern)}
</div>
<div className="text-sm text-gray-600">
{metrics.summary.trendsAnalysis.productivityPattern === 'consistent' ? 'Régulier' :
metrics.summary.trendsAnalysis.productivityPattern === 'variable' ? 'Variable' : 'Weekend+'}
</div>
</div>
</div>
</CardContent>
</Card>
{/* Graphiques principaux */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">📈 Évolution quotidienne des statuts</h3>
</CardHeader>
<CardContent>
<DailyStatusChart data={metrics.dailyBreakdown} />
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">🎯 Taux de completion quotidien</h3>
</CardHeader>
<CardContent>
<CompletionRateChart data={metrics.dailyBreakdown} />
</CardContent>
</Card>
</div>
{/* Distribution et priorités */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">🍰 Répartition des statuts</h3>
</CardHeader>
<CardContent>
<StatusDistributionChart data={metrics.statusDistribution} />
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="text-lg font-semibold"> Performance par priorité</h3>
</CardHeader>
<CardContent>
<PriorityBreakdownChart data={metrics.priorityBreakdown} />
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">🔥 Heatmap d&apos;activité</h3>
</CardHeader>
<CardContent>
<WeeklyActivityHeatmap data={metrics.dailyBreakdown} />
</CardContent>
</Card>
</div>
{/* Tendances de vélocité */}
{trends.length > 0 && (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">🚀 Tendances de vélocité</h3>
<select
value={weeksBack}
onChange={(e) => setWeeksBack(parseInt(e.target.value))}
className="text-sm border border-[var(--border)] rounded px-2 py-1 bg-[var(--background)]"
>
<option value={4}>4 semaines</option>
<option value={8}>8 semaines</option>
<option value={12}>12 semaines</option>
</select>
</div>
</CardHeader>
<CardContent>
<VelocityTrendChart data={trends} />
</CardContent>
</Card>
)}
{/* Analyses de productivité */}
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">💡 Analyses de productivité</h3>
</CardHeader>
<CardContent>
<ProductivityInsights data={metrics.dailyBreakdown} />
</CardContent>
</Card>
</div>
) : null}
</div>
);
}

View File

@@ -1,162 +0,0 @@
'use client';
import { useState, useEffect, useTransition } from 'react';
import { ProductivityMetrics } from '@/services/analytics';
import { getProductivityMetrics } from '@/actions/analytics';
import { CompletionTrendChart } from '@/components/charts/CompletionTrendChart';
import { VelocityChart } from '@/components/charts/VelocityChart';
import { PriorityDistributionChart } from '@/components/charts/PriorityDistributionChart';
import { WeeklyStatsCard } from '@/components/charts/WeeklyStatsCard';
import { Card } from '@/components/ui/Card';
export function ProductivityAnalytics() {
const [metrics, setMetrics] = useState<ProductivityMetrics | null>(null);
const [error, setError] = useState<string | null>(null);
const [isPending, startTransition] = useTransition();
useEffect(() => {
const loadMetrics = () => {
startTransition(async () => {
try {
setError(null);
const response = await getProductivityMetrics();
if (response.success && response.data) {
setMetrics(response.data);
} else {
setError(response.error || 'Erreur lors du chargement des métriques');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Erreur lors du chargement des métriques');
console.error('Erreur analytics:', err);
}
});
};
loadMetrics();
}, []);
if (isPending) {
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
{Array.from({ length: 4 }).map((_, i) => (
<Card key={i} className="p-6 animate-pulse">
<div className="h-4 bg-[var(--border)] rounded mb-4 w-1/3"></div>
<div className="h-64 bg-[var(--border)] rounded"></div>
</Card>
))}
</div>
);
}
if (error) {
return (
<Card className="p-6 mb-8 mt-8">
<div className="text-center">
<div className="text-red-500 text-4xl mb-2"></div>
<h3 className="text-lg font-semibold mb-2">Erreur de chargement</h3>
<p className="text-[var(--muted-foreground)] text-sm">{error}</p>
</div>
</Card>
);
}
if (!metrics) {
return null;
}
return (
<div className="space-y-8">
{/* Titre de section */}
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold">📊 Analytics & Métriques</h2>
<div className="text-sm text-[var(--muted-foreground)]">
Derniers 30 jours
</div>
</div>
{/* Performance hebdomadaire */}
<WeeklyStatsCard stats={metrics.weeklyStats} />
{/* Graphiques principaux */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<CompletionTrendChart data={metrics.completionTrend} />
<VelocityChart data={metrics.velocityData} />
</div>
{/* Distributions */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<PriorityDistributionChart data={metrics.priorityDistribution} />
{/* Status Flow - Graphique simple en barres horizontales */}
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4">Répartition par Statut</h3>
<div className="space-y-3">
{metrics.statusFlow.map((item, index) => (
<div key={index} className="flex items-center gap-3">
<div className="w-20 text-sm text-[var(--muted-foreground)] text-right">
{item.status}
</div>
<div className="flex-1 bg-[var(--border)] rounded-full h-2 relative">
<div
className="bg-gradient-to-r from-blue-500 to-cyan-500 h-2 rounded-full transition-all duration-300"
style={{ width: `${item.percentage}%` }}
></div>
</div>
<div className="w-12 text-sm font-medium text-right">
{item.count}
</div>
<div className="w-10 text-xs text-[var(--muted-foreground)] text-right">
{item.percentage}%
</div>
</div>
))}
</div>
</Card>
</div>
{/* Insights automatiques */}
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4">💡 Insights</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-[var(--card)] p-4 rounded-lg border border-[var(--border)] hover:border-[var(--primary)]/50 transition-colors">
<div className="text-[var(--primary)] font-medium text-sm mb-1">
Vélocité Moyenne
</div>
<div className="text-2xl font-bold text-[var(--foreground)]">
{metrics.velocityData.length > 0
? Math.round(metrics.velocityData.reduce((acc, item) => acc + item.completed, 0) / metrics.velocityData.length)
: 0
} <span className="text-sm font-normal text-[var(--muted-foreground)]">tâches/sem</span>
</div>
</div>
<div className="bg-[var(--card)] p-4 rounded-lg border border-[var(--border)] hover:border-[var(--success)]/50 transition-colors">
<div className="text-[var(--success)] font-medium text-sm mb-1">
Priorité Principale
</div>
<div className="text-lg font-bold text-[var(--foreground)]">
{metrics.priorityDistribution.reduce((max, item) =>
item.count > max.count ? item : max,
metrics.priorityDistribution[0]
)?.priority || 'N/A'}
</div>
</div>
<div className="bg-[var(--card)] p-4 rounded-lg border border-[var(--border)] hover:border-[var(--accent)]/50 transition-colors">
<div className="text-[var(--accent)] font-medium text-sm mb-1">
Taux de Completion
</div>
<div className="text-2xl font-bold text-[var(--foreground)]">
{(() => {
const completed = metrics.statusFlow.find(s => s.status === 'Terminé')?.count || 0;
const total = metrics.statusFlow.reduce((acc, s) => acc + s.count, 0);
return total > 0 ? Math.round((completed / total) * 100) : 0;
})()}%
</div>
</div>
</div>
</Card>
</div>
);
}

View File

@@ -1,93 +0,0 @@
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/Button';
import { CreateTaskForm } from '@/components/forms/CreateTaskForm';
import { CreateTaskData } from '@/clients/tasks-client';
import Link from 'next/link';
interface QuickActionsProps {
onCreateTask: (data: CreateTaskData) => Promise<void>;
}
export function QuickActions({ onCreateTask }: QuickActionsProps) {
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const handleCreateTask = async (data: CreateTaskData) => {
await onCreateTask(data);
setIsCreateModalOpen(false);
};
return (
<>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<Button
variant="primary"
onClick={() => setIsCreateModalOpen(true)}
className="flex items-center gap-2 p-6 h-auto"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
<div className="text-left">
<div className="font-semibold">Nouvelle Tâche</div>
<div className="text-sm opacity-80">Créer une nouvelle tâche</div>
</div>
</Button>
<Link href="/kanban">
<Button
variant="secondary"
className="flex items-center gap-2 p-6 h-auto w-full"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 0V5a2 2 0 012-2h2a2 2 0 002-2" />
</svg>
<div className="text-left">
<div className="font-semibold">Kanban Board</div>
<div className="text-sm opacity-80">Gérer les tâches</div>
</div>
</Button>
</Link>
<Link href="/daily">
<Button
variant="secondary"
className="flex items-center gap-2 p-6 h-auto w-full"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<div className="text-left">
<div className="font-semibold">Daily</div>
<div className="text-sm opacity-80">Checkboxes quotidiennes</div>
</div>
</Button>
</Link>
<Link href="/settings">
<Button
variant="secondary"
className="flex items-center gap-2 p-6 h-auto w-full"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<div className="text-left">
<div className="font-semibold">Paramètres</div>
<div className="text-sm opacity-80">Configuration</div>
</div>
</Button>
</Link>
</div>
<CreateTaskForm
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
onSubmit={handleCreateTask}
loading={false}
/>
</>
);
}

View File

@@ -1,131 +0,0 @@
'use client';
import { Task } from '@/lib/types';
import { Card } from '@/components/ui/Card';
import { TagDisplay } from '@/components/ui/TagDisplay';
import { Badge } from '@/components/ui/Badge';
import { useTasksContext } from '@/contexts/TasksContext';
import { getPriorityConfig, getPriorityColorHex, getStatusBadgeClasses, getStatusLabel } from '@/lib/status-config';
import { TaskPriority } from '@/lib/types';
import Link from 'next/link';
interface RecentTasksProps {
tasks: Task[];
}
export function RecentTasks({ tasks }: RecentTasksProps) {
const { tags: availableTags } = useTasksContext();
// Prendre les 5 tâches les plus récentes (créées ou modifiées)
const recentTasks = tasks
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
.slice(0, 5);
// Fonctions simplifiées utilisant la configuration centralisée
const getPriorityStyle = (priority: string) => {
try {
const config = getPriorityConfig(priority as TaskPriority);
const hexColor = getPriorityColorHex(config.color);
return { color: hexColor };
} catch {
return { color: '#6b7280' }; // gray-500 par défaut
}
};
return (
<Card className="p-6 mt-8">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">Tâches Récentes</h3>
<Link href="/kanban">
<button className="text-sm text-[var(--primary)] hover:underline">
Voir toutes
</button>
</Link>
</div>
{recentTasks.length === 0 ? (
<div className="text-center py-8 text-[var(--muted-foreground)]">
<svg className="w-12 h-12 mx-auto mb-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<p>Aucune tâche disponible</p>
<p className="text-sm">Créez votre première tâche pour commencer</p>
</div>
) : (
<div className="space-y-3">
{recentTasks.map((task) => (
<div
key={task.id}
className="p-3 border border-[var(--border)] rounded-lg hover:bg-[var(--card)]/50 transition-colors"
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h4 className="font-medium text-sm truncate">{task.title}</h4>
{task.source === 'jira' && (
<Badge variant="outline" className="text-xs">
Jira
</Badge>
)}
</div>
{task.description && (
<p className="text-xs text-[var(--muted-foreground)] mb-2 line-clamp-1">
{task.description}
</p>
)}
<div className="flex items-center gap-2 flex-wrap">
<Badge className={`text-xs ${getStatusBadgeClasses(task.status)}`}>
{getStatusLabel(task.status)}
</Badge>
{task.priority && (
<span
className="text-xs font-medium"
style={getPriorityStyle(task.priority)}
>
{(() => {
try {
return getPriorityConfig(task.priority as TaskPriority).label;
} catch {
return task.priority;
}
})()}
</span>
)}
{task.tags && task.tags.length > 0 && (
<div className="flex gap-1">
<TagDisplay
tags={task.tags.slice(0, 2)}
availableTags={availableTags}
size="sm"
maxTags={2}
showColors={true}
/>
{task.tags.length > 2 && (
<span className="text-xs text-[var(--muted-foreground)]">
+{task.tags.length - 2}
</span>
)}
</div>
)}
</div>
</div>
<div className="text-xs text-[var(--muted-foreground)] whitespace-nowrap">
{new Date(task.updatedAt).toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'short'
})}
</div>
</div>
</div>
))}
</div>
)}
</Card>
);
}

View File

@@ -1,274 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
import { Modal } from '@/components/ui/Modal';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { TagInput } from '@/components/ui/TagInput';
import { RelatedTodos } from '@/components/forms/RelatedTodos';
import { Badge } from '@/components/ui/Badge';
import { Task, TaskPriority, TaskStatus } from '@/lib/types';
import { useUserPreferences } from '@/contexts/UserPreferencesContext';
// UpdateTaskData removed - using Server Actions directly
import { getAllStatuses, getAllPriorities } from '@/lib/status-config';
interface EditTaskFormProps {
isOpen: boolean;
onClose: () => void;
onSubmit: (data: { taskId: string; title?: string; description?: string; status?: TaskStatus; priority?: TaskPriority; tags?: string[]; dueDate?: Date; }) => Promise<void>;
task: Task | null;
loading?: boolean;
}
export function EditTaskForm({ isOpen, onClose, onSubmit, task, loading = false }: EditTaskFormProps) {
const { preferences } = useUserPreferences();
const [formData, setFormData] = useState<{
title: string;
description: string;
status: TaskStatus;
priority: TaskPriority;
tags: string[];
dueDate?: Date;
}>({
title: '',
description: '',
status: 'todo' as TaskStatus,
priority: 'medium' as TaskPriority,
tags: [],
dueDate: undefined
});
const [errors, setErrors] = useState<Record<string, string>>({});
// Helper pour construire l'URL Jira
const getJiraTicketUrl = (jiraKey: string): string => {
const baseUrl = preferences.jiraConfig.baseUrl;
if (!baseUrl || !jiraKey) return '';
return `${baseUrl}/browse/${jiraKey}`;
};
// Pré-remplir le formulaire quand la tâche change
useEffect(() => {
if (task) {
setFormData({
title: task.title,
description: task.description || '',
status: task.status,
priority: task.priority,
tags: task.tags || [],
dueDate: task.dueDate ? new Date(task.dueDate) : undefined
});
}
}, [task]);
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {};
if (!formData.title?.trim()) {
newErrors.title = 'Le titre est requis';
}
if (formData.title && formData.title.length > 200) {
newErrors.title = 'Le titre ne peut pas dépasser 200 caractères';
}
if (formData.description && formData.description.length > 1000) {
newErrors.description = 'La description ne peut pas dépasser 1000 caractères';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm() || !task) return;
try {
await onSubmit({
taskId: task.id,
...formData
});
handleClose();
} catch (error) {
console.error('Erreur lors de la mise à jour:', error);
}
};
const handleClose = () => {
setErrors({});
onClose();
};
if (!task) return null;
return (
<Modal isOpen={isOpen} onClose={handleClose} title="Modifier la tâche" size="lg">
<form onSubmit={handleSubmit} className="space-y-4 max-h-[80vh] overflow-y-auto pr-2">
{/* Titre */}
<Input
label="Titre *"
value={formData.title}
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
placeholder="Titre de la tâche..."
error={errors.title}
disabled={loading}
/>
{/* Description */}
<div className="space-y-2">
<label className="block text-sm font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
Description
</label>
<textarea
value={formData.description}
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
placeholder="Description détaillée..."
rows={4}
disabled={loading}
className="w-full px-3 py-2 bg-[var(--input)] border border-[var(--border)]/50 rounded-lg text-[var(--foreground)] font-mono text-sm placeholder-[var(--muted-foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)]/50 focus:border-[var(--primary)]/50 hover:border-[var(--border)] transition-all duration-200 backdrop-blur-sm resize-none"
/>
{errors.description && (
<p className="text-xs font-mono text-red-400 flex items-center gap-1">
<span className="text-red-500"></span>
{errors.description}
</p>
)}
</div>
{/* Priorité et Statut */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="block text-sm font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
Priorité
</label>
<select
value={formData.priority}
onChange={(e) => setFormData(prev => ({ ...prev, priority: e.target.value as TaskPriority }))}
disabled={loading}
className="w-full px-3 py-2 bg-[var(--input)] border border-[var(--border)]/50 rounded-lg text-[var(--foreground)] font-mono text-sm focus:outline-none focus:ring-2 focus:ring-[var(--primary)]/50 focus:border-[var(--primary)]/50 hover:border-[var(--border)] transition-all duration-200 backdrop-blur-sm"
>
{getAllPriorities().map(priorityConfig => (
<option key={priorityConfig.key} value={priorityConfig.key}>
{priorityConfig.icon} {priorityConfig.label}
</option>
))}
</select>
</div>
<div className="space-y-2">
<label className="block text-sm font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
Statut
</label>
<select
value={formData.status}
onChange={(e) => setFormData(prev => ({ ...prev, status: e.target.value as TaskStatus }))}
disabled={loading}
className="w-full px-3 py-2 bg-[var(--input)] border border-[var(--border)]/50 rounded-lg text-[var(--foreground)] font-mono text-sm focus:outline-none focus:ring-2 focus:ring-[var(--primary)]/50 focus:border-[var(--primary)]/50 hover:border-[var(--border)] transition-all duration-200 backdrop-blur-sm"
>
{getAllStatuses().map(statusConfig => (
<option key={statusConfig.key} value={statusConfig.key}>
{statusConfig.label}
</option>
))}
</select>
</div>
</div>
{/* Date d'échéance */}
<Input
label="Date d'échéance"
type="datetime-local"
value={formData.dueDate ? new Date(formData.dueDate.getTime() - formData.dueDate.getTimezoneOffset() * 60000).toISOString().slice(0, 16) : ''}
onChange={(e) => setFormData(prev => ({
...prev,
dueDate: e.target.value ? new Date(e.target.value) : undefined
}))}
disabled={loading}
/>
{/* Informations Jira */}
{task.source === 'jira' && task.jiraKey && (
<div className="space-y-3">
<label className="block text-sm font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
Jira
</label>
<div className="flex items-center gap-3">
{preferences.jiraConfig.baseUrl ? (
<a
href={getJiraTicketUrl(task.jiraKey)}
target="_blank"
rel="noopener noreferrer"
className="hover:scale-105 transition-transform inline-flex"
>
<Badge
variant="outline"
size="sm"
className="hover:bg-blue-500/10 hover:border-blue-400/50 cursor-pointer"
>
{task.jiraKey}
</Badge>
</a>
) : (
<Badge variant="outline" size="sm">
{task.jiraKey}
</Badge>
)}
{task.jiraProject && (
<Badge variant="outline" size="sm" className="text-blue-400 border-blue-400/30">
{task.jiraProject}
</Badge>
)}
{task.jiraType && (
<Badge variant="outline" size="sm" className="text-purple-400 border-purple-400/30">
{task.jiraType}
</Badge>
)}
</div>
</div>
)}
{/* Tags */}
<div className="space-y-3">
<label className="block text-sm font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
Tags
</label>
<TagInput
tags={formData.tags || []}
onChange={(tags) => setFormData(prev => ({ ...prev, tags }))}
placeholder="Ajouter des tags..."
maxTags={10}
/>
</div>
{/* Todos reliés */}
<RelatedTodos taskId={task.id} />
{/* Actions */}
<div className="flex justify-end gap-3 pt-4 border-t border-[var(--border)]/50">
<Button
type="button"
variant="ghost"
onClick={handleClose}
disabled={loading}
>
Annuler
</Button>
<Button
type="submit"
variant="primary"
disabled={loading}
>
{loading ? 'Mise à jour...' : 'Mettre à jour'}
</Button>
</div>
</form>
</Modal>
);
}

View File

@@ -1,327 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
import { JiraAnalyticsFilters, AvailableFilters, FilterOption } from '@/lib/types';
import { JiraAdvancedFiltersService } from '@/services/jira-advanced-filters';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { Modal } from '@/components/ui/Modal';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
interface AdvancedFiltersPanelProps {
availableFilters: AvailableFilters;
activeFilters: Partial<JiraAnalyticsFilters>;
onFiltersChange: (filters: Partial<JiraAnalyticsFilters>) => void;
className?: string;
}
interface FilterSectionProps {
title: string;
icon: string;
options: FilterOption[];
selectedValues: string[];
onSelectionChange: (values: string[]) => void;
maxDisplay?: number;
}
function FilterSection({ title, icon, options, selectedValues, onSelectionChange, maxDisplay = 10 }: FilterSectionProps) {
const [showAll, setShowAll] = useState(false);
const displayOptions = showAll ? options : options.slice(0, maxDisplay);
const hasMore = options.length > maxDisplay;
const handleToggle = (value: string) => {
const newValues = selectedValues.includes(value)
? selectedValues.filter(v => v !== value)
: [...selectedValues, value];
onSelectionChange(newValues);
};
const selectAll = () => {
onSelectionChange(options.map(opt => opt.value));
};
const clearAll = () => {
onSelectionChange([]);
};
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<h4 className="font-medium text-sm flex items-center gap-2">
<span>{icon}</span>
{title}
{selectedValues.length > 0 && (
<Badge className="bg-blue-100 text-blue-800 text-xs">
{selectedValues.length}
</Badge>
)}
</h4>
{options.length > 0 && (
<div className="flex gap-1">
<button
onClick={selectAll}
className="text-xs text-blue-600 hover:text-blue-800"
>
Tout
</button>
<span className="text-xs text-gray-400">|</span>
<button
onClick={clearAll}
className="text-xs text-gray-600 hover:text-gray-800"
>
Aucun
</button>
</div>
)}
</div>
{options.length === 0 ? (
<p className="text-sm text-gray-500 italic">Aucune option disponible</p>
) : (
<>
<div className="space-y-1 max-h-32 overflow-y-auto">
{displayOptions.map(option => (
<label
key={option.value}
className="flex items-center gap-2 text-sm cursor-pointer hover:bg-gray-50 px-2 py-1 rounded"
>
<input
type="checkbox"
checked={selectedValues.includes(option.value)}
onChange={() => handleToggle(option.value)}
className="rounded"
/>
<span className="flex-1 truncate">{option.label}</span>
<span className="text-xs text-gray-500">({option.count})</span>
</label>
))}
</div>
{hasMore && (
<button
onClick={() => setShowAll(!showAll)}
className="text-xs text-blue-600 hover:text-blue-800"
>
{showAll ? `Afficher moins` : `Afficher ${options.length - maxDisplay} de plus`}
</button>
)}
</>
)}
</div>
);
}
export default function AdvancedFiltersPanel({
availableFilters,
activeFilters,
onFiltersChange,
className = ''
}: AdvancedFiltersPanelProps) {
const [showModal, setShowModal] = useState(false);
const [tempFilters, setTempFilters] = useState<Partial<JiraAnalyticsFilters>>(activeFilters);
useEffect(() => {
setTempFilters(activeFilters);
}, [activeFilters]);
const hasActiveFilters = JiraAdvancedFiltersService.hasActiveFilters(activeFilters);
const activeFiltersCount = JiraAdvancedFiltersService.countActiveFilters(activeFilters);
const filtersSummary = JiraAdvancedFiltersService.getFiltersSummary(activeFilters);
const applyFilters = () => {
onFiltersChange(tempFilters);
setShowModal(false);
};
const clearAllFilters = () => {
const emptyFilters = JiraAdvancedFiltersService.createEmptyFilters();
setTempFilters(emptyFilters);
onFiltersChange(emptyFilters);
setShowModal(false);
};
const updateTempFilter = <K extends keyof JiraAnalyticsFilters>(
key: K,
value: JiraAnalyticsFilters[K]
) => {
setTempFilters(prev => ({
...prev,
[key]: value
}));
};
return (
<Card className={className}>
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<h3 className="font-semibold">🔍 Filtres avancés</h3>
{hasActiveFilters && (
<Badge className="bg-blue-100 text-blue-800 text-xs">
{activeFiltersCount} actif{activeFiltersCount > 1 ? 's' : ''}
</Badge>
)}
</div>
<div className="flex gap-2">
{hasActiveFilters && (
<Button
onClick={clearAllFilters}
variant="secondary"
size="sm"
className="text-xs"
>
🗑 Effacer
</Button>
)}
<Button
onClick={() => setShowModal(true)}
size="sm"
className="text-xs"
>
Configurer
</Button>
</div>
</div>
<p className="text-sm text-[var(--muted-foreground)] mt-1">
{filtersSummary}
</p>
</CardHeader>
{/* Aperçu rapide des filtres actifs */}
{hasActiveFilters && (
<CardContent className="pt-0">
<div className="p-3 bg-blue-50 rounded-lg">
<div className="flex flex-wrap gap-1">
{activeFilters.components?.map(comp => (
<Badge key={comp} className="bg-purple-100 text-purple-800 text-xs">
📦 {comp}
</Badge>
))}
{activeFilters.fixVersions?.map(version => (
<Badge key={version} className="bg-green-100 text-green-800 text-xs">
🏷 {version}
</Badge>
))}
{activeFilters.issueTypes?.map(type => (
<Badge key={type} className="bg-orange-100 text-orange-800 text-xs">
📋 {type}
</Badge>
))}
{activeFilters.statuses?.map(status => (
<Badge key={status} className="bg-blue-100 text-blue-800 text-xs">
🔄 {status}
</Badge>
))}
{activeFilters.assignees?.map(assignee => (
<Badge key={assignee} className="bg-yellow-100 text-yellow-800 text-xs">
👤 {assignee}
</Badge>
))}
{activeFilters.labels?.map(label => (
<Badge key={label} className="bg-gray-100 text-gray-800 text-xs">
🏷 {label}
</Badge>
))}
{activeFilters.priorities?.map(priority => (
<Badge key={priority} className="bg-red-100 text-red-800 text-xs">
{priority}
</Badge>
))}
</div>
</div>
</CardContent>
)}
{/* Modal de configuration des filtres */}
<Modal
isOpen={showModal}
onClose={() => setShowModal(false)}
title="Configuration des filtres avancés"
size="lg"
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-h-96 overflow-y-auto">
<FilterSection
title="Composants"
icon="📦"
options={availableFilters.components}
selectedValues={tempFilters.components || []}
onSelectionChange={(values) => updateTempFilter('components', values)}
/>
<FilterSection
title="Versions"
icon="🏷️"
options={availableFilters.fixVersions}
selectedValues={tempFilters.fixVersions || []}
onSelectionChange={(values) => updateTempFilter('fixVersions', values)}
/>
<FilterSection
title="Types de tickets"
icon="📋"
options={availableFilters.issueTypes}
selectedValues={tempFilters.issueTypes || []}
onSelectionChange={(values) => updateTempFilter('issueTypes', values)}
/>
<FilterSection
title="Statuts"
icon="🔄"
options={availableFilters.statuses}
selectedValues={tempFilters.statuses || []}
onSelectionChange={(values) => updateTempFilter('statuses', values)}
/>
<FilterSection
title="Assignés"
icon="👤"
options={availableFilters.assignees}
selectedValues={tempFilters.assignees || []}
onSelectionChange={(values) => updateTempFilter('assignees', values)}
/>
<FilterSection
title="Labels"
icon="🏷️"
options={availableFilters.labels}
selectedValues={tempFilters.labels || []}
onSelectionChange={(values) => updateTempFilter('labels', values)}
/>
<FilterSection
title="Priorités"
icon="⚡"
options={availableFilters.priorities}
selectedValues={tempFilters.priorities || []}
onSelectionChange={(values) => updateTempFilter('priorities', values)}
/>
</div>
<div className="flex gap-2 pt-6 border-t">
<Button
onClick={applyFilters}
className="flex-1"
>
Appliquer les filtres
</Button>
<Button
onClick={clearAllFilters}
variant="secondary"
className="flex-1"
>
🗑 Effacer tout
</Button>
<Button
onClick={() => setShowModal(false)}
variant="secondary"
>
Annuler
</Button>
</div>
</Modal>
</Card>
);
}

View File

@@ -1,334 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
import { detectJiraAnomalies, updateAnomalyDetectionConfig, getAnomalyDetectionConfig } from '@/actions/jira-anomalies';
import { JiraAnomaly, AnomalyDetectionConfig } from '@/services/jira-anomaly-detection';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { Modal } from '@/components/ui/Modal';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
interface AnomalyDetectionPanelProps {
className?: string;
}
export default function AnomalyDetectionPanel({ className = '' }: AnomalyDetectionPanelProps) {
const [anomalies, setAnomalies] = useState<JiraAnomaly[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [showConfig, setShowConfig] = useState(false);
const [config, setConfig] = useState<AnomalyDetectionConfig | null>(null);
const [lastUpdate, setLastUpdate] = useState<string | null>(null);
const [isExpanded, setIsExpanded] = useState(false);
// Charger la config au montage, les anomalies seulement si expanded
useEffect(() => {
loadConfig();
}, []);
// Charger les anomalies quand on ouvre le panneau
useEffect(() => {
if (isExpanded && anomalies.length === 0) {
loadAnomalies();
}
}, [isExpanded, anomalies.length]);
const loadAnomalies = async (forceRefresh = false) => {
setLoading(true);
setError(null);
try {
const result = await detectJiraAnomalies(forceRefresh);
if (result.success && result.data) {
setAnomalies(result.data);
setLastUpdate(new Date().toLocaleString('fr-FR'));
} else {
setError(result.error || 'Erreur lors de la détection');
}
} catch {
setError('Erreur de connexion');
} finally {
setLoading(false);
}
};
const loadConfig = async () => {
try {
const result = await getAnomalyDetectionConfig();
if (result.success && result.data) {
setConfig(result.data);
}
} catch (err) {
console.error('Erreur lors du chargement de la config:', err);
}
};
const handleConfigUpdate = async (newConfig: AnomalyDetectionConfig) => {
try {
const result = await updateAnomalyDetectionConfig(newConfig);
if (result.success && result.data) {
setConfig(result.data);
setShowConfig(false);
// Recharger les anomalies avec la nouvelle config
loadAnomalies(true);
}
} catch (err) {
console.error('Erreur lors de la mise à jour de la config:', err);
}
};
const getSeverityColor = (severity: string): string => {
switch (severity) {
case 'critical': return 'bg-red-100 text-red-800 border-red-200';
case 'high': return 'bg-orange-100 text-orange-800 border-orange-200';
case 'medium': return 'bg-yellow-100 text-yellow-800 border-yellow-200';
case 'low': return 'bg-blue-100 text-blue-800 border-blue-200';
default: return 'bg-gray-100 text-gray-800 border-gray-200';
}
};
const getSeverityIcon = (severity: string): string => {
switch (severity) {
case 'critical': return '🚨';
case 'high': return '⚠️';
case 'medium': return '⚡';
case 'low': return '';
default: return '📊';
}
};
const criticalCount = anomalies.filter(a => a.severity === 'critical').length;
const highCount = anomalies.filter(a => a.severity === 'high').length;
const totalCount = anomalies.length;
return (
<Card className={className}>
<CardHeader
className="cursor-pointer hover:bg-[var(--muted)] transition-colors"
onClick={() => setIsExpanded(!isExpanded)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="transition-transform duration-200" style={{ transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)' }}>
</span>
<h3 className="font-semibold">🔍 Détection d&apos;anomalies</h3>
{totalCount > 0 && (
<div className="flex gap-1">
{criticalCount > 0 && (
<Badge className="bg-red-100 text-red-800 text-xs">
{criticalCount} critique{criticalCount > 1 ? 's' : ''}
</Badge>
)}
{highCount > 0 && (
<Badge className="bg-orange-100 text-orange-800 text-xs">
{highCount} élevée{highCount > 1 ? 's' : ''}
</Badge>
)}
</div>
)}
</div>
{isExpanded && (
<div className="flex gap-2" onClick={(e) => e.stopPropagation()}>
<Button
onClick={() => setShowConfig(true)}
variant="secondary"
size="sm"
className="text-xs"
>
Config
</Button>
<Button
onClick={() => loadAnomalies(true)}
disabled={loading}
size="sm"
className="text-xs"
>
{loading ? '🔄' : '🔍'} {loading ? 'Analyse...' : 'Analyser'}
</Button>
</div>
)}
</div>
{isExpanded && lastUpdate && (
<p className="text-xs text-[var(--muted-foreground)] mt-1">
Dernière analyse: {lastUpdate}
</p>
)}
</CardHeader>
{isExpanded && (
<CardContent>
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-3 mb-4">
<p className="text-red-700 text-sm"> {error}</p>
</div>
)}
{loading && (
<div className="flex items-center justify-center py-8">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div>
<p className="text-sm text-gray-600">Analyse en cours...</p>
</div>
</div>
)}
{!loading && !error && anomalies.length === 0 && (
<div className="text-center py-8">
<div className="text-4xl mb-2"></div>
<p className="text-[var(--foreground)] font-medium">Aucune anomalie détectée</p>
<p className="text-sm text-[var(--muted-foreground)]">Toutes les métriques sont dans les seuils normaux</p>
</div>
)}
{!loading && anomalies.length > 0 && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
{anomalies.map((anomaly) => (
<div
key={anomaly.id}
className="border border-[var(--border)] rounded-lg p-3 bg-[var(--card)] hover:bg-[var(--muted)] transition-colors"
>
<div className="flex items-start gap-2">
<span className="text-sm">{getSeverityIcon(anomaly.severity)}</span>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h4 className="font-medium text-sm truncate">{anomaly.title}</h4>
<Badge className={`text-xs shrink-0 ${getSeverityColor(anomaly.severity)}`}>
{anomaly.severity}
</Badge>
</div>
<p className="text-xs text-[var(--muted-foreground)] mb-2 line-clamp-2">{anomaly.description}</p>
<div className="text-xs text-[var(--muted-foreground)]">
<strong>Valeur:</strong> {anomaly.value.toFixed(1)}
{anomaly.threshold > 0 && (
<span className="opacity-75"> (seuil: {anomaly.threshold.toFixed(1)})</span>
)}
</div>
{anomaly.affectedItems.length > 0 && (
<div className="mt-2">
<div className="text-xs text-[var(--muted-foreground)]">
{anomaly.affectedItems.slice(0, 2).map((item, index) => (
<span key={index} className="inline-block bg-[var(--muted)] rounded px-1 mr-1 mb-1 text-xs">
{item}
</span>
))}
{anomaly.affectedItems.length > 2 && (
<span className="text-xs opacity-75">+{anomaly.affectedItems.length - 2}</span>
)}
</div>
</div>
)}
</div>
</div>
</div>
))}
</div>
)}
</CardContent>
)}
{/* Modal de configuration */}
{showConfig && config && (
<Modal
isOpen={showConfig}
onClose={() => setShowConfig(false)}
title="Configuration de la détection d'anomalies"
>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Seuil de variance de vélocité (%)
</label>
<input
type="number"
value={config.velocityVarianceThreshold}
onChange={(e) => setConfig({...config, velocityVarianceThreshold: Number(e.target.value)})}
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
min="0"
max="100"
/>
<p className="text-xs text-gray-500 mt-1">
Pourcentage de variance acceptable dans la vélocité
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Multiplicateur de cycle time
</label>
<input
type="number"
step="0.1"
value={config.cycleTimeThreshold}
onChange={(e) => setConfig({...config, cycleTimeThreshold: Number(e.target.value)})}
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
min="1"
max="5"
/>
<p className="text-xs text-gray-500 mt-1">
Multiplicateur au-delà duquel le cycle time est considéré anormal
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Ratio de déséquilibre de charge
</label>
<input
type="number"
step="0.1"
value={config.workloadImbalanceThreshold}
onChange={(e) => setConfig({...config, workloadImbalanceThreshold: Number(e.target.value)})}
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
min="1"
max="10"
/>
<p className="text-xs text-gray-500 mt-1">
Ratio maximum acceptable entre les charges de travail
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Taux de completion minimum (%)
</label>
<input
type="number"
value={config.completionRateThreshold}
onChange={(e) => setConfig({...config, completionRateThreshold: Number(e.target.value)})}
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
min="0"
max="100"
/>
<p className="text-xs text-gray-500 mt-1">
Pourcentage minimum de completion des sprints
</p>
</div>
<div className="flex gap-2 pt-4">
<Button
onClick={() => handleConfigUpdate(config)}
className="flex-1"
>
💾 Sauvegarder
</Button>
<Button
onClick={() => setShowConfig(false)}
variant="secondary"
className="flex-1"
>
Annuler
</Button>
</div>
</div>
</Modal>
)}
</Card>
);
}

View File

@@ -1,269 +0,0 @@
'use client';
import React from 'react';
import { JiraAnalytics } from '@/lib/types';
import { Card } from '@/components/ui/Card';
interface CollaborationMatrixProps {
analytics: JiraAnalytics;
className?: string;
}
interface CollaborationData {
assignee: string;
displayName: string;
collaborationScore: number;
dependencies: Array<{
partner: string;
partnerDisplayName: string;
sharedTickets: number;
intensity: 'low' | 'medium' | 'high';
}>;
isolation: number; // Score d'isolation (0-100, plus c'est élevé plus isolé)
}
export function CollaborationMatrix({ analytics, className }: CollaborationMatrixProps) {
// Analyser les patterns de collaboration basés sur les données existantes
const collaborationData: CollaborationData[] = analytics.teamMetrics.issuesDistribution.map(assignee => {
// Simuler des collaborations basées sur les données réelles
const totalTickets = assignee.totalIssues;
// Générer des partenaires de collaboration réalistes
const otherAssignees = analytics.teamMetrics.issuesDistribution.filter(a => a.assignee !== assignee.assignee);
const dependencies = otherAssignees
.slice(0, Math.min(3, otherAssignees.length)) // Maximum 3 collaborations principales
.map(partner => {
// Simuler un nombre de tickets partagés basé sur la taille relative des équipes
const maxShared = Math.min(totalTickets, partner.totalIssues);
const sharedTickets = Math.floor(Math.random() * Math.max(1, maxShared * 0.3));
const intensity: 'low' | 'medium' | 'high' =
sharedTickets > maxShared * 0.2 ? 'high' :
sharedTickets > maxShared * 0.1 ? 'medium' : 'low';
return {
partner: partner.assignee,
partnerDisplayName: partner.displayName,
sharedTickets,
intensity
};
})
.filter(dep => dep.sharedTickets > 0)
.sort((a, b) => b.sharedTickets - a.sharedTickets);
// Calculer le score de collaboration (basé sur le nombre de collaborations)
const collaborationScore = dependencies.reduce((score, dep) => score + dep.sharedTickets, 0);
// Calculer l'isolation (inverse de la collaboration)
const maxPossibleCollaboration = totalTickets * 0.5; // 50% max de collaboration
const isolation = Math.max(0, 100 - (collaborationScore / maxPossibleCollaboration) * 100);
return {
assignee: assignee.assignee,
displayName: assignee.displayName,
collaborationScore,
dependencies,
isolation: Math.round(isolation)
};
});
// Statistiques globales
const avgCollaboration = collaborationData.reduce((sum, d) => sum + d.collaborationScore, 0) / collaborationData.length;
const avgIsolation = collaborationData.reduce((sum, d) => sum + d.isolation, 0) / collaborationData.length;
const mostCollaborative = collaborationData.reduce((max, current) =>
current.collaborationScore > max.collaborationScore ? current : max, collaborationData[0]);
const mostIsolated = collaborationData.reduce((max, current) =>
current.isolation > max.isolation ? current : max, collaborationData[0]);
// Couleur d'intensité
const getIntensityColor = (intensity: 'low' | 'medium' | 'high') => {
switch (intensity) {
case 'high': return 'bg-green-600 dark:bg-green-500';
case 'medium': return 'bg-yellow-600 dark:bg-yellow-500';
case 'low': return 'bg-gray-500 dark:bg-gray-400';
}
};
return (
<div className={className}>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Matrice de collaboration */}
<div className="lg:col-span-2">
<h4 className="text-sm font-medium mb-3">Réseau de collaboration</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 max-h-96 overflow-y-auto">
{collaborationData.map(person => (
<Card key={person.assignee} className="p-3">
<div className="flex items-center justify-between mb-2">
<div className="font-medium text-sm">{person.displayName}</div>
<div className="flex items-center gap-2">
<span className="text-xs text-[var(--muted-foreground)]">
Score: {person.collaborationScore}
</span>
<div className={`w-3 h-3 rounded-full ${
person.isolation < 30 ? 'bg-green-600 dark:bg-green-500' :
person.isolation < 60 ? 'bg-yellow-600 dark:bg-yellow-500' : 'bg-red-600 dark:bg-red-500'
}`} />
</div>
</div>
<div className="space-y-1">
{person.dependencies.length > 0 ? (
person.dependencies.map(dep => (
<div key={dep.partner} className="flex items-center justify-between text-xs">
<span className="text-[var(--muted-foreground)] truncate">
{dep.partnerDisplayName}
</span>
<div className="flex items-center gap-2 flex-shrink-0">
<span>{dep.sharedTickets} tickets</span>
<div className={`w-2 h-2 rounded-full ${getIntensityColor(dep.intensity)}`} />
</div>
</div>
))
) : (
<div className="text-xs text-[var(--muted-foreground)] italic">
Aucune collaboration détectée
</div>
)}
</div>
</Card>
))}
</div>
</div>
{/* Métriques de collaboration */}
<div>
<h4 className="text-sm font-medium mb-3">Analyse d&apos;équipe</h4>
<div className="space-y-4">
{/* Graphique de répartition */}
<Card className="p-3">
<h5 className="text-xs font-medium mb-2">Répartition par niveau</h5>
<div className="space-y-2">
{['Très collaboratif', 'Collaboratif', 'Isolé', 'Très isolé'].map((level, index) => {
const ranges = [[0, 30], [30, 50], [50, 70], [70, 100]];
const [min, max] = ranges[index];
const count = collaborationData.filter(d => d.isolation >= min && d.isolation < max).length;
const colors = ['bg-green-600 dark:bg-green-500', 'bg-blue-600 dark:bg-blue-500', 'bg-yellow-600 dark:bg-yellow-500', 'bg-red-600 dark:bg-red-500'];
return (
<div key={level} className="flex items-center gap-2 text-xs">
<div className={`w-3 h-3 rounded-sm ${colors[index]}`} />
<span className="flex-1 truncate">{level}</span>
<span className="font-mono text-xs">{count}</span>
</div>
);
})}
</div>
</Card>
{/* Insights */}
<Card className="p-3">
<h5 className="text-xs font-medium mb-2">🏆 Plus collaboratif</h5>
<div className="text-sm">
<div className="font-medium truncate">{mostCollaborative?.displayName}</div>
<div className="text-xs text-[var(--muted-foreground)]">
{mostCollaborative?.collaborationScore} interactions
</div>
</div>
</Card>
<Card className="p-3">
<h5 className="text-xs font-medium mb-2"> Plus isolé</h5>
<div className="text-sm">
<div className="font-medium truncate">{mostIsolated?.displayName}</div>
<div className="text-xs text-[var(--muted-foreground)]">
{mostIsolated?.isolation}% d&apos;isolation
</div>
</div>
</Card>
{/* Légende des intensités */}
<Card className="p-3">
<h5 className="text-xs font-medium mb-2">Légende</h5>
<div className="space-y-1">
{[
{ intensity: 'high' as const, label: 'Forte' },
{ intensity: 'medium' as const, label: 'Modérée' },
{ intensity: 'low' as const, label: 'Faible' }
].map(item => (
<div key={item.intensity} className="flex items-center gap-2 text-xs">
<div className={`w-2 h-2 rounded-full ${getIntensityColor(item.intensity)}`} />
<span>{item.label}</span>
</div>
))}
</div>
</Card>
</div>
</div>
</div>
{/* Métriques globales */}
<div className="mt-6 grid grid-cols-4 gap-4">
<div className="text-center p-3 bg-[var(--card)] rounded-lg border border-[var(--border)]">
<div className="text-lg font-bold text-blue-500">
{Math.round(avgCollaboration)}
</div>
<div className="text-xs text-[var(--muted-foreground)]">
Collaboration moyenne
</div>
</div>
<div className="text-center p-3 bg-[var(--card)] rounded-lg border border-[var(--border)]">
<div className={`text-lg font-bold ${avgIsolation < 40 ? 'text-green-500' : avgIsolation < 60 ? 'text-orange-500' : 'text-red-500'}`}>
{Math.round(avgIsolation)}%
</div>
<div className="text-xs text-[var(--muted-foreground)]">
Isolation moyenne
</div>
</div>
<div className="text-center p-3 bg-[var(--card)] rounded-lg border border-[var(--border)]">
<div className="text-lg font-bold text-purple-500">
{collaborationData.filter(d => d.dependencies.length > 0).length}
</div>
<div className="text-xs text-[var(--muted-foreground)]">
Membres connectés
</div>
</div>
<div className="text-center p-3 bg-[var(--card)] rounded-lg border border-[var(--border)]">
<div className="text-lg font-bold text-indigo-500">
{collaborationData.reduce((sum, d) => sum + d.dependencies.length, 0)}
</div>
<div className="text-xs text-[var(--muted-foreground)]">
Connexions totales
</div>
</div>
</div>
{/* Recommandations */}
<div className="mt-4 p-4 bg-[var(--card)] rounded-lg border border-[var(--border)]">
<h4 className="text-sm font-medium mb-2">Recommandations d&apos;équipe</h4>
<div className="space-y-2 text-sm">
{avgIsolation > 60 && (
<div className="flex items-center gap-2 text-red-600 dark:text-red-400">
<span></span>
<span>Isolation élevée - Encourager le pair programming et les reviews croisées</span>
</div>
)}
{avgIsolation < 30 && (
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
<span></span>
<span>Excellente collaboration - L&apos;équipe travaille bien ensemble</span>
</div>
)}
{mostIsolated && mostIsolated.isolation > 80 && (
<div className="flex items-center gap-2 text-orange-600 dark:text-orange-400">
<span>👥</span>
<span>Attention à {mostIsolated.displayName} - Considérer du mentoring ou du binômage</span>
</div>
)}
{collaborationData.filter(d => d.dependencies.length === 0).length > 0 && (
<div className="flex items-center gap-2 text-blue-600 dark:text-blue-400">
<span>🔗</span>
<span>Quelques membres travaillent en silo - Organiser des sessions de partage</span>
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -1,341 +0,0 @@
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/Button';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import { Modal } from '@/components/ui/Modal';
import { jiraClient } from '@/clients/jira-client';
import { JiraSyncResult, JiraSyncAction } from '@/services/jira';
interface JiraSyncProps {
onSyncComplete?: () => void;
className?: string;
}
export function JiraSync({ onSyncComplete, className = "" }: JiraSyncProps) {
const [isConnected, setIsConnected] = useState<boolean | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isSyncing, setIsSyncing] = useState(false);
const [lastSyncResult, setLastSyncResult] = useState<JiraSyncResult | null>(null);
const [error, setError] = useState<string | null>(null);
const [showDetails, setShowDetails] = useState(false);
const testConnection = async () => {
setIsLoading(true);
setError(null);
try {
const status = await jiraClient.testConnection();
setIsConnected(status.connected);
if (!status.connected) {
setError(status.message);
}
} catch (err) {
setIsConnected(false);
setError(err instanceof Error ? err.message : 'Erreur de connexion');
} finally {
setIsLoading(false);
}
};
const startSync = async () => {
setIsSyncing(true);
setError(null);
try {
const result = await jiraClient.syncTasks();
setLastSyncResult(result);
if (result.success) {
onSyncComplete?.();
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Erreur de synchronisation');
} finally {
setIsSyncing(false);
}
};
const getConnectionStatus = () => {
if (isConnected === null) return null;
return isConnected ? (
<Badge variant="success" size="sm"> Connecté</Badge>
) : (
<Badge variant="danger" size="sm"> Déconnecté</Badge>
);
};
const getSyncStatus = () => {
if (!lastSyncResult) return null;
const { success, tasksFound, tasksCreated, tasksUpdated, tasksSkipped, tasksDeleted = 0, errors, actions = [] } = lastSyncResult;
return (
<div className="space-y-3 text-sm">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Badge variant={success ? "success" : "danger"} size="sm">
{success ? "✓ Succès" : "⚠ Erreurs"}
</Badge>
<span className="text-[var(--muted-foreground)] text-xs">
{new Date().toLocaleTimeString()}
</span>
</div>
<div className="text-xs text-[var(--muted-foreground)]">
{tasksFound} trouvé{tasksFound > 1 ? 's' : ''} dans Jira
</div>
</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div className="text-center p-2 bg-[var(--card)] rounded">
<div className="font-mono font-bold text-emerald-400">{tasksCreated}</div>
<div className="text-[var(--muted-foreground)]">Créées</div>
</div>
<div className="text-center p-2 bg-[var(--card)] rounded">
<div className="font-mono font-bold text-blue-400">{tasksUpdated}</div>
<div className="text-[var(--muted-foreground)]">Mises à jour</div>
</div>
<div className="text-center p-2 bg-[var(--card)] rounded">
<div className="font-mono font-bold text-orange-400">{tasksSkipped}</div>
<div className="text-[var(--muted-foreground)]">Ignorées</div>
</div>
<div className="text-center p-2 bg-[var(--card)] rounded">
<div className="font-mono font-bold text-red-400">{tasksDeleted}</div>
<div className="text-[var(--muted-foreground)]">Supprimées</div>
</div>
</div>
{/* Résumé textuel avec bouton détails */}
<div className="p-2 bg-[var(--muted)]/5 rounded text-xs">
<div className="flex items-center justify-between mb-1">
<div className="font-medium text-[var(--muted-foreground)]">Résumé:</div>
{actions.length > 0 && (
<Button
onClick={() => setShowDetails(true)}
variant="secondary"
size="sm"
className="text-xs px-2 py-1 h-auto"
>
Voir détails ({actions.length})
</Button>
)}
</div>
<div className="text-[var(--muted-foreground)]">
{tasksCreated > 0 && `${tasksCreated} nouvelle${tasksCreated > 1 ? 's' : ''}`}
{tasksUpdated > 0 && `${tasksUpdated} mise${tasksUpdated > 1 ? 's' : ''} à jour • `}
{tasksDeleted > 0 && `${tasksDeleted} supprimée${tasksDeleted > 1 ? 's' : ''} (réassignées) • `}
{tasksSkipped > 0 && `${tasksSkipped} ignorée${tasksSkipped > 1 ? 's' : ''}`}
{(tasksCreated + tasksUpdated + tasksDeleted + tasksSkipped) === 0 && 'Aucune modification'}
</div>
</div>
{errors.length > 0 && (
<div className="p-2 bg-[var(--destructive)]/10 border border-[var(--destructive)]/20 rounded text-xs">
<div className="font-semibold text-[var(--destructive)] mb-1">Erreurs ({errors.length}):</div>
<div className="space-y-1 max-h-20 overflow-y-auto">
{errors.map((err, i) => (
<div key={i} className="text-[var(--destructive)] font-mono text-xs">{err}</div>
))}
</div>
</div>
)}
</div>
);
};
return (
<Card className={`${className}`}>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-2 h-2 rounded-full bg-blue-500 dark:bg-blue-400 animate-pulse"></div>
<h3 className="font-mono text-sm font-bold text-blue-400 uppercase tracking-wider">
JIRA SYNC
</h3>
</div>
{getConnectionStatus()}
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Test de connexion */}
<div className="flex gap-2">
<Button
onClick={testConnection}
disabled={isLoading}
variant="secondary"
size="sm"
className="flex-1"
>
{isLoading ? (
<div className="flex items-center gap-2">
<div className="w-3 h-3 border border-[var(--muted-foreground)] border-t-transparent rounded-full animate-spin"></div>
Test...
</div>
) : (
'Tester connexion'
)}
</Button>
<Button
onClick={startSync}
disabled={isSyncing || isConnected === false}
variant="primary"
size="sm"
className="flex-1"
>
{isSyncing ? (
<div className="flex items-center gap-2">
<div className="w-3 h-3 border border-white border-t-transparent rounded-full animate-spin"></div>
Sync...
</div>
) : (
'🔄 Synchroniser'
)}
</Button>
</div>
{/* Messages d'erreur */}
{error && (
<div className="p-3 bg-[var(--destructive)]/10 border border-[var(--destructive)]/20 rounded text-sm text-[var(--destructive)]">
{error}
</div>
)}
{/* Résultats de sync */}
{getSyncStatus()}
{/* Info */}
<div className="text-xs text-[var(--muted-foreground)] space-y-1">
<div> Synchronisation unidirectionnelle (Jira TowerControl)</div>
<div> Les modifications locales sont préservées</div>
<div> Seuls les tickets assignés sont synchronisés</div>
<div> Les tickets réassignés sont automatiquement supprimés</div>
</div>
</CardContent>
{/* Modal détails de synchronisation */}
{lastSyncResult && (
<Modal
isOpen={showDetails}
onClose={() => setShowDetails(false)}
title="📋 DÉTAILS DE SYNCHRONISATION"
size="xl"
>
<div className="space-y-4">
<p className="text-sm text-[var(--muted-foreground)]">
{(lastSyncResult.actions || []).length} action{(lastSyncResult.actions || []).length > 1 ? 's' : ''} effectuée{(lastSyncResult.actions || []).length > 1 ? 's' : ''}
</p>
<div className="max-h-[60vh] overflow-y-auto">
{(lastSyncResult.actions || []).length > 0 ? (
<SyncActionsList actions={lastSyncResult.actions || []} />
) : (
<div className="text-center py-8 text-[var(--muted-foreground)]">
<div className="text-2xl mb-2">📝</div>
<div>Aucun détail disponible pour cette synchronisation</div>
<div className="text-sm mt-1">Les détails sont disponibles pour les nouvelles synchronisations</div>
</div>
)}
</div>
</div>
</Modal>
)}
</Card>
);
}
// Composant pour afficher la liste des actions
function SyncActionsList({ actions }: { actions: JiraSyncAction[] }) {
const getActionIcon = (type: JiraSyncAction['type']) => {
switch (type) {
case 'created': return '';
case 'updated': return '🔄';
case 'skipped': return '⏭️';
case 'deleted': return '🗑️';
default: return '❓';
}
};
const getActionColor = (type: JiraSyncAction['type']) => {
switch (type) {
case 'created': return 'text-emerald-400';
case 'updated': return 'text-blue-400';
case 'skipped': return 'text-orange-400';
case 'deleted': return 'text-red-400';
default: return 'text-gray-400';
}
};
const getActionLabel = (type: JiraSyncAction['type']) => {
switch (type) {
case 'created': return 'Créée';
case 'updated': return 'Mise à jour';
case 'skipped': return 'Ignorée';
case 'deleted': return 'Supprimée';
default: return 'Inconnue';
}
};
// Grouper les actions par type
const groupedActions = actions.reduce((acc, action) => {
if (!acc[action.type]) acc[action.type] = [];
acc[action.type].push(action);
return acc;
}, {} as Record<string, JiraSyncAction[]>);
return (
<div className="space-y-6">
{Object.entries(groupedActions).map(([type, typeActions]) => (
<div key={type} className="space-y-3">
<h4 className={`font-bold text-sm flex items-center gap-2 ${getActionColor(type as JiraSyncAction['type'])}`}>
{getActionIcon(type as JiraSyncAction['type'])}
{getActionLabel(type as JiraSyncAction['type'])} ({typeActions.length})
</h4>
<div className="space-y-2">
{typeActions.map((action, index) => (
<div key={index} className="p-2 bg-[var(--muted)]/10 rounded border border-[var(--border)]">
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-baseline gap-2">
<span className="font-mono text-sm font-bold text-[var(--foreground)] shrink-0">
{action.taskKey}
</span>
<span className="text-sm text-[var(--muted-foreground)] truncate">
{action.taskTitle}
</span>
</div>
</div>
<Badge variant="outline" size="sm" className="shrink-0">
{getActionLabel(action.type)}
</Badge>
</div>
{action.reason && (
<div className="mt-1 text-xs text-[var(--muted-foreground)] italic">
💡 {action.reason}
</div>
)}
{action.changes && action.changes.length > 0 && (
<div className="mt-1 space-y-0.5">
<div className="text-xs font-medium text-[var(--muted-foreground)]">
Modifications:
</div>
{action.changes.map((change, changeIndex) => (
<div key={changeIndex} className="text-xs font-mono text-[var(--foreground)] pl-2 border-l-2 border-blue-400/30">
{change}
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
))}
</div>
);
}

View File

@@ -1,241 +0,0 @@
'use client';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar, Cell } from 'recharts';
import { SprintVelocity } from '@/lib/types';
interface PredictabilityMetricsProps {
sprintHistory: SprintVelocity[];
className?: string;
}
interface PredictabilityDataPoint {
sprint: string;
planned: number;
actual: number;
variance: number; // Pourcentage de variance (positif = dépassement, négatif = sous-performance)
accuracy: number; // Pourcentage d'exactitude (100% = parfait)
}
export function PredictabilityMetrics({ sprintHistory, className }: PredictabilityMetricsProps) {
// Calculer les métriques de predictabilité
const predictabilityData: PredictabilityDataPoint[] = sprintHistory.map(sprint => {
const variance = sprint.plannedPoints > 0
? ((sprint.completedPoints - sprint.plannedPoints) / sprint.plannedPoints) * 100
: 0;
const accuracy = sprint.plannedPoints > 0
? Math.max(0, 100 - Math.abs(variance))
: 0;
return {
sprint: sprint.sprintName.replace('Sprint ', ''),
planned: sprint.plannedPoints,
actual: sprint.completedPoints,
variance: Math.round(variance * 10) / 10,
accuracy: Math.round(accuracy * 10) / 10
};
});
// Calculer les statistiques globales
const averageVariance = predictabilityData.length > 0
? predictabilityData.reduce((sum, d) => sum + Math.abs(d.variance), 0) / predictabilityData.length
: 0;
const averageAccuracy = predictabilityData.length > 0
? predictabilityData.reduce((sum, d) => sum + d.accuracy, 0) / predictabilityData.length
: 0;
const consistencyScore = averageVariance < 10 ? 'Excellent' :
averageVariance < 20 ? 'Bon' :
averageVariance < 30 ? 'Moyen' : 'À améliorer';
// Tendance de l'exactitude (en amélioration ou dégradation)
const recentAccuracy = predictabilityData.slice(-2);
const trend = recentAccuracy.length >= 2
? recentAccuracy[1].accuracy - recentAccuracy[0].accuracy
: 0;
const CustomTooltip = ({ active, payload, label }: {
active?: boolean;
payload?: Array<{ payload: PredictabilityDataPoint; value: number; name: string; color: string }>;
label?: string
}) => {
if (active && payload && payload.length) {
const data = payload[0].payload;
return (
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 shadow-lg">
<p className="font-medium text-sm mb-2">Sprint {label}</p>
<div className="space-y-1 text-xs">
<div className="flex justify-between gap-4">
<span>Planifié:</span>
<span className="font-mono text-gray-500">{data.planned} pts</span>
</div>
<div className="flex justify-between gap-4">
<span>Réalisé:</span>
<span className="font-mono text-blue-500">{data.actual} pts</span>
</div>
<div className="flex justify-between gap-4">
<span>Variance:</span>
<span className={`font-mono ${data.variance > 0 ? 'text-green-500' : data.variance < 0 ? 'text-red-500' : 'text-gray-500'}`}>
{data.variance > 0 ? '+' : ''}{data.variance}%
</span>
</div>
<div className="flex justify-between gap-4">
<span>Exactitude:</span>
<span className="font-mono text-orange-500">{data.accuracy}%</span>
</div>
</div>
</div>
);
}
return null;
};
return (
<div className={className}>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Graphique de variance */}
<div>
<h4 className="text-sm font-medium mb-3">Variance planifié vs réalisé</h4>
<div style={{ width: '100%', height: '200px' }}>
<ResponsiveContainer width="100%" height="100%">
<BarChart data={predictabilityData} margin={{ top: 20, right: 30, left: 20, bottom: 20 }}>
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
<XAxis
dataKey="sprint"
stroke="var(--muted-foreground)"
fontSize={10}
/>
<YAxis
stroke="var(--muted-foreground)"
fontSize={10}
label={{ value: '%', angle: 0, position: 'insideLeft' }}
/>
<Tooltip content={<CustomTooltip />} />
<Bar
dataKey="variance"
radius={[2, 2, 2, 2]}
>
{predictabilityData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={entry.variance > 0 ? 'hsl(142, 76%, 36%)' : entry.variance < 0 ? 'hsl(0, 84%, 60%)' : 'hsl(240, 5%, 64%)'}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
</div>
{/* Graphique d'exactitude */}
<div>
<h4 className="text-sm font-medium mb-3">Évolution de l&apos;exactitude</h4>
<div style={{ width: '100%', height: '200px' }}>
<ResponsiveContainer width="100%" height="100%">
<LineChart data={predictabilityData} margin={{ top: 20, right: 30, left: 20, bottom: 20 }}>
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
<XAxis
dataKey="sprint"
stroke="var(--muted-foreground)"
fontSize={10}
/>
<YAxis
stroke="var(--muted-foreground)"
fontSize={10}
domain={[0, 100]}
label={{ value: '%', angle: 0, position: 'insideLeft' }}
/>
<Tooltip content={<CustomTooltip />} />
<Line
type="monotone"
dataKey="accuracy"
stroke="hsl(45, 93%, 47%)"
strokeWidth={3}
dot={{ fill: 'hsl(45, 93%, 47%)', strokeWidth: 2, r: 4 }}
name="Exactitude"
/>
</LineChart>
</ResponsiveContainer>
</div>
</div>
</div>
{/* Métriques de predictabilité */}
<div className="mt-6 grid grid-cols-4 gap-4">
<div className="text-center p-3 bg-[var(--card)] rounded-lg border border-[var(--border)]">
<div className={`text-lg font-bold ${averageAccuracy > 80 ? 'text-green-500' : averageAccuracy > 60 ? 'text-orange-500' : 'text-red-500'}`}>
{Math.round(averageAccuracy)}%
</div>
<div className="text-xs text-[var(--muted-foreground)]">
Exactitude moyenne
</div>
</div>
<div className="text-center p-3 bg-[var(--card)] rounded-lg border border-[var(--border)]">
<div className={`text-lg font-bold ${averageVariance < 10 ? 'text-green-500' : averageVariance < 20 ? 'text-orange-500' : 'text-red-500'}`}>
{Math.round(averageVariance * 10) / 10}%
</div>
<div className="text-xs text-[var(--muted-foreground)]">
Variance moyenne
</div>
</div>
<div className="text-center p-3 bg-[var(--card)] rounded-lg border border-[var(--border)]">
<div className={`text-lg font-bold ${consistencyScore === 'Excellent' ? 'text-green-500' : consistencyScore === 'Bon' ? 'text-blue-500' : consistencyScore === 'Moyen' ? 'text-orange-500' : 'text-red-500'}`}>
{consistencyScore}
</div>
<div className="text-xs text-[var(--muted-foreground)]">
Consistance
</div>
</div>
<div className="text-center p-3 bg-[var(--card)] rounded-lg border border-[var(--border)]">
<div className={`text-lg font-bold ${trend > 5 ? 'text-green-500' : trend < -5 ? 'text-red-500' : 'text-blue-500'}`}>
{trend > 0 ? '↗️' : trend < 0 ? '↘️' : '→'} {Math.abs(Math.round(trend))}%
</div>
<div className="text-xs text-[var(--muted-foreground)]">
Tendance récente
</div>
</div>
</div>
{/* Analyse et recommandations */}
<div className="mt-4 p-4 bg-[var(--card)] rounded-lg border border-[var(--border)]">
<h4 className="text-sm font-medium mb-2">Analyse de predictabilité</h4>
<div className="space-y-2 text-sm">
{averageAccuracy > 80 && (
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
<span></span>
<span>Excellente predictabilité - L&apos;équipe estime bien sa capacité</span>
</div>
)}
{averageAccuracy < 60 && (
<div className="flex items-center gap-2 text-red-600 dark:text-red-400">
<span></span>
<span>Predictabilité faible - Revoir les méthodes d&apos;estimation</span>
</div>
)}
{averageVariance > 25 && (
<div className="flex items-center gap-2 text-orange-600 dark:text-orange-400">
<span>📊</span>
<span>Variance élevée - Considérer des sprints plus courts ou un meilleur découpage</span>
</div>
)}
{trend > 10 && (
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
<span>📈</span>
<span>Tendance positive - L&apos;équipe s&apos;améliore dans ses estimations</span>
</div>
)}
{trend < -10 && (
<div className="flex items-center gap-2 text-red-600 dark:text-red-400">
<span>📉</span>
<span>Tendance négative - Attention aux changements récents (équipe, processus)</span>
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -1,425 +0,0 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { SprintVelocity, JiraTask, AssigneeDistribution, StatusDistribution } from '@/lib/types';
import { Modal } from '@/components/ui/Modal';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import { Button } from '@/components/ui/Button';
interface SprintDetailModalProps {
isOpen: boolean;
onClose: () => void;
sprint: SprintVelocity | null;
onLoadSprintDetails: (sprintName: string) => Promise<SprintDetails>;
}
export interface SprintDetails {
sprint: SprintVelocity;
issues: JiraTask[];
assigneeDistribution: AssigneeDistribution[];
statusDistribution: StatusDistribution[];
metrics: {
totalIssues: number;
completedIssues: number;
inProgressIssues: number;
blockedIssues: number;
averageCycleTime: number;
velocityTrend: 'up' | 'down' | 'stable';
};
}
export default function SprintDetailModal({
isOpen,
onClose,
sprint,
onLoadSprintDetails
}: SprintDetailModalProps) {
const [sprintDetails, setSprintDetails] = useState<SprintDetails | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedTab, setSelectedTab] = useState<'overview' | 'issues' | 'metrics'>('overview');
const [selectedAssignee, setSelectedAssignee] = useState<string | null>(null);
const [selectedStatus, setSelectedStatus] = useState<string | null>(null);
const loadSprintDetails = useCallback(async () => {
if (!sprint) return;
setLoading(true);
setError(null);
try {
const details = await onLoadSprintDetails(sprint.sprintName);
setSprintDetails(details);
} catch (err) {
setError(err instanceof Error ? err.message : 'Erreur lors du chargement');
} finally {
setLoading(false);
}
}, [sprint, onLoadSprintDetails]);
// Charger les détails du sprint quand le modal s'ouvre
useEffect(() => {
if (isOpen && sprint && !sprintDetails) {
loadSprintDetails();
}
}, [isOpen, sprint, sprintDetails, loadSprintDetails]);
// Reset quand on change de sprint
useEffect(() => {
if (sprint) {
setSprintDetails(null);
setSelectedAssignee(null);
setSelectedStatus(null);
setSelectedTab('overview');
}
}, [sprint]);
// Filtrer les issues selon les sélections
const filteredIssues = sprintDetails?.issues.filter(issue => {
if (selectedAssignee && (issue.assignee?.displayName || 'Non assigné') !== selectedAssignee) {
return false;
}
if (selectedStatus && issue.status.name !== selectedStatus) {
return false;
}
return true;
}) || [];
const getStatusColor = (status: string): string => {
if (status.toLowerCase().includes('done') || status.toLowerCase().includes('closed')) {
return 'bg-green-100 text-green-800';
}
if (status.toLowerCase().includes('progress') || status.toLowerCase().includes('review')) {
return 'bg-blue-100 text-blue-800';
}
if (status.toLowerCase().includes('blocked') || status.toLowerCase().includes('waiting')) {
return 'bg-red-100 text-red-800';
}
return 'bg-gray-100 text-gray-800';
};
const getPriorityColor = (priority?: string): string => {
switch (priority?.toLowerCase()) {
case 'highest': return 'bg-red-500 text-white';
case 'high': return 'bg-orange-500 text-white';
case 'medium': return 'bg-yellow-500 text-white';
case 'low': return 'bg-green-500 text-white';
case 'lowest': return 'bg-gray-500 text-white';
default: return 'bg-gray-300 text-gray-800';
}
};
if (!sprint) return null;
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title={`Sprint: ${sprint.sprintName}`}
size="lg"
>
<div className="space-y-6">
{/* En-tête du sprint */}
<div className="bg-gray-50 rounded-lg p-4">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-blue-600">
{sprint.completedPoints}
</div>
<div className="text-sm text-gray-600">Points complétés</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-gray-800">
{sprint.plannedPoints}
</div>
<div className="text-sm text-gray-600">Points planifiés</div>
</div>
<div className="text-center">
<div className={`text-2xl font-bold ${sprint.completionRate >= 80 ? 'text-green-600' : sprint.completionRate >= 60 ? 'text-orange-600' : 'text-red-600'}`}>
{sprint.completionRate.toFixed(1)}%
</div>
<div className="text-sm text-gray-600">Taux de completion</div>
</div>
<div className="text-center">
<div className="text-sm text-gray-600">Période</div>
<div className="text-xs text-gray-500">
{new Date(sprint.startDate).toLocaleDateString('fr-FR')} - {new Date(sprint.endDate).toLocaleDateString('fr-FR')}
</div>
</div>
</div>
</div>
{/* Onglets */}
<div className="border-b border-gray-200">
<nav className="flex space-x-8">
{[
{ id: 'overview', label: '📊 Vue d\'ensemble', icon: '📊' },
{ id: 'issues', label: '📋 Tickets', icon: '📋' },
{ id: 'metrics', label: '📈 Métriques', icon: '📈' }
].map(tab => (
<button
key={tab.id}
onClick={() => setSelectedTab(tab.id as 'overview' | 'issues' | 'metrics')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
selectedTab === tab.id
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
{tab.label}
</button>
))}
</nav>
</div>
{/* Contenu selon l'onglet */}
{loading && (
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">Chargement des détails du sprint...</p>
</div>
</div>
)}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-red-700"> {error}</p>
<Button onClick={loadSprintDetails} className="mt-2" size="sm">
Réessayer
</Button>
</div>
)}
{!loading && !error && sprintDetails && (
<>
{/* Vue d'ensemble */}
{selectedTab === 'overview' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<h3 className="font-semibold">👥 Répartition par assigné</h3>
</CardHeader>
<CardContent>
<div className="space-y-2">
{sprintDetails.assigneeDistribution.map(assignee => (
<div
key={assignee.assignee}
className={`flex items-center justify-between p-2 rounded cursor-pointer transition-colors ${
selectedAssignee === assignee.displayName
? 'bg-blue-100'
: 'hover:bg-gray-50'
}`}
onClick={() => setSelectedAssignee(
selectedAssignee === assignee.displayName ? null : assignee.displayName
)}
>
<span className="font-medium">{assignee.displayName}</span>
<div className="flex gap-2">
<Badge className="bg-green-100 text-green-800 text-xs">
{assignee.completedIssues}
</Badge>
<Badge className="bg-blue-100 text-blue-800 text-xs">
🔄 {assignee.inProgressIssues}
</Badge>
<Badge className="bg-gray-100 text-gray-800 text-xs">
📋 {assignee.totalIssues}
</Badge>
</div>
</div>
))}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="font-semibold">🔄 Répartition par statut</h3>
</CardHeader>
<CardContent>
<div className="space-y-2">
{sprintDetails.statusDistribution.map(status => (
<div
key={status.status}
className={`flex items-center justify-between p-2 rounded cursor-pointer transition-colors ${
selectedStatus === status.status
? 'bg-blue-100'
: 'hover:bg-gray-50'
}`}
onClick={() => setSelectedStatus(
selectedStatus === status.status ? null : status.status
)}
>
<span className="font-medium">{status.status}</span>
<div className="flex gap-2">
<Badge className={`text-xs ${getStatusColor(status.status)}`}>
{status.count} ({status.percentage.toFixed(1)}%)
</Badge>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
)}
{/* Liste des tickets */}
{selectedTab === 'issues' && (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="font-semibold text-lg">
📋 Tickets du sprint ({filteredIssues.length})
</h3>
<div className="flex gap-2">
{selectedAssignee && (
<Badge className="bg-blue-100 text-blue-800">
👤 {selectedAssignee}
<button
onClick={() => setSelectedAssignee(null)}
className="ml-1 text-blue-600 hover:text-blue-800"
>
×
</button>
</Badge>
)}
{selectedStatus && (
<Badge className="bg-purple-100 text-purple-800">
🔄 {selectedStatus}
<button
onClick={() => setSelectedStatus(null)}
className="ml-1 text-purple-600 hover:text-purple-800"
>
×
</button>
</Badge>
)}
</div>
</div>
<div className="space-y-2 max-h-96 overflow-y-auto">
{filteredIssues.map(issue => (
<div key={issue.id} className="border rounded-lg p-3 hover:bg-gray-50">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="font-mono text-sm text-blue-600">{issue.key}</span>
<Badge className={`text-xs ${getStatusColor(issue.status.name)}`}>
{issue.status.name}
</Badge>
{issue.priority && (
<Badge className={`text-xs ${getPriorityColor(issue.priority.name)}`}>
{issue.priority.name}
</Badge>
)}
</div>
<h4 className="font-medium text-sm mb-1">{issue.summary}</h4>
<div className="flex items-center gap-4 text-xs text-gray-500">
<span>📋 {issue.issuetype.name}</span>
<span>👤 {issue.assignee?.displayName || 'Non assigné'}</span>
<span>📅 {new Date(issue.created).toLocaleDateString('fr-FR')}</span>
</div>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Métriques détaillées */}
{selectedTab === 'metrics' && (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<Card>
<CardHeader>
<h3 className="font-semibold">📊 Métriques générales</h3>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="flex justify-between">
<span>Total tickets:</span>
<span className="font-semibold">{sprintDetails.metrics.totalIssues}</span>
</div>
<div className="flex justify-between">
<span>Tickets complétés:</span>
<span className="font-semibold text-green-600">{sprintDetails.metrics.completedIssues}</span>
</div>
<div className="flex justify-between">
<span>En cours:</span>
<span className="font-semibold text-blue-600">{sprintDetails.metrics.inProgressIssues}</span>
</div>
<div className="flex justify-between">
<span>Cycle time moyen:</span>
<span className="font-semibold">{sprintDetails.metrics.averageCycleTime.toFixed(1)} jours</span>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="font-semibold">📈 Tendance vélocité</h3>
</CardHeader>
<CardContent>
<div className="text-center">
<div className={`text-4xl mb-2 ${
sprintDetails.metrics.velocityTrend === 'up' ? 'text-green-600' :
sprintDetails.metrics.velocityTrend === 'down' ? 'text-red-600' :
'text-gray-600'
}`}>
{sprintDetails.metrics.velocityTrend === 'up' ? '📈' :
sprintDetails.metrics.velocityTrend === 'down' ? '📉' : '➡️'}
</div>
<p className="text-sm text-gray-600">
{sprintDetails.metrics.velocityTrend === 'up' ? 'En progression' :
sprintDetails.metrics.velocityTrend === 'down' ? 'En baisse' : 'Stable'}
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="font-semibold"> Points d&apos;attention</h3>
</CardHeader>
<CardContent>
<div className="space-y-2 text-sm">
{sprint.completionRate < 70 && (
<div className="text-red-600">
Taux de completion faible ({sprint.completionRate.toFixed(1)}%)
</div>
)}
{sprintDetails.metrics.blockedIssues > 0 && (
<div className="text-orange-600">
{sprintDetails.metrics.blockedIssues} ticket(s) bloqué(s)
</div>
)}
{sprintDetails.metrics.averageCycleTime > 14 && (
<div className="text-yellow-600">
Cycle time élevé ({sprintDetails.metrics.averageCycleTime.toFixed(1)} jours)
</div>
)}
{sprint.completionRate >= 90 && sprintDetails.metrics.blockedIssues === 0 && (
<div className="text-green-600">
Sprint réussi sans blockers majeurs
</div>
)}
</div>
</CardContent>
</Card>
</div>
)}
</>
)}
{/* Actions */}
<div className="flex justify-end">
<Button onClick={onClose} variant="secondary">
Fermer
</Button>
</div>
</div>
</Modal>
);
}

View File

@@ -1,141 +0,0 @@
'use client';
import { useState } from 'react';
import { KanbanBoard } from './Board';
import { SwimlanesBoard } from './SwimlanesBoard';
import { PrioritySwimlanesBoard } from './PrioritySwimlanesBoard';
import { ObjectivesBoard } from './ObjectivesBoard';
import { KanbanFilters } from './KanbanFilters';
import { EditTaskForm } from '@/components/forms/EditTaskForm';
import { useTasksContext } from '@/contexts/TasksContext';
import { useUserPreferences } from '@/contexts/UserPreferencesContext';
import { Task, TaskStatus, TaskPriority } from '@/lib/types';
import { CreateTaskData } from '@/clients/tasks-client';
import { updateTask, createTask } from '@/actions/tasks';
import { getAllStatuses } from '@/lib/status-config';
interface KanbanBoardContainerProps {
showFilters?: boolean;
showObjectives?: boolean;
}
export function KanbanBoardContainer({
showFilters = true,
showObjectives = true
}: KanbanBoardContainerProps = {}) {
const {
filteredTasks,
pinnedTasks,
loading,
updateTaskOptimistic,
kanbanFilters,
setKanbanFilters,
tags,
refreshTasks
} = useTasksContext();
const { preferences, toggleColumnVisibility, isColumnVisible } = useUserPreferences();
const allStatuses = getAllStatuses();
const visibleStatuses = allStatuses.filter(status => isColumnVisible(status.key)).map(s => s.key);
const [editingTask, setEditingTask] = useState<Task | null>(null);
const handleEditTask = (task: Task) => {
setEditingTask(task);
};
const handleUpdateTask = async (data: { taskId: string; title?: string; description?: string; status?: TaskStatus; priority?: TaskPriority; tags?: string[]; dueDate?: Date; }) => {
const result = await updateTask(data);
if (result.success) {
await refreshTasks(); // Rafraîchir les données
setEditingTask(null);
} else {
console.error('Error updating task:', result.error);
}
};
const handleUpdateStatus = async (taskId: string, newStatus: TaskStatus) => {
// Utiliser la mise à jour optimiste pour le drag & drop
await updateTaskOptimistic(taskId, newStatus);
};
// Obtenir le nom du tag épinglé pour l'affichage
const pinnedTagName = tags.find(tag => tag.isPinned)?.name;
// Wrapper pour adapter le type de createTask
const handleCreateTask = async (data: CreateTaskData): Promise<void> => {
const result = await createTask(data);
if (result.success) {
await refreshTasks(); // Rafraîchir les données
} else {
console.error('Error creating task:', result.error);
}
};
return (
<>
{/* Barre de filtres - conditionnelle */}
{showFilters && (
<KanbanFilters
filters={kanbanFilters}
onFiltersChange={setKanbanFilters}
hiddenStatuses={new Set(preferences.columnVisibility.hiddenStatuses)}
onToggleStatusVisibility={toggleColumnVisibility}
/>
)}
{/* Section Objectifs Principaux - conditionnelle */}
{showObjectives && pinnedTasks.length > 0 && (
<ObjectivesBoard
tasks={pinnedTasks}
onEditTask={handleEditTask}
onUpdateStatus={handleUpdateStatus}
compactView={kanbanFilters.compactView}
pinnedTagName={pinnedTagName}
/>
)}
{kanbanFilters.swimlanesByTags ? (
kanbanFilters.swimlanesMode === 'priority' ? (
<PrioritySwimlanesBoard
tasks={filteredTasks}
onCreateTask={handleCreateTask}
onEditTask={handleEditTask}
onUpdateStatus={handleUpdateStatus}
compactView={kanbanFilters.compactView}
visibleStatuses={visibleStatuses}
loading={loading}
/>
) : (
<SwimlanesBoard
tasks={filteredTasks}
onCreateTask={handleCreateTask}
onEditTask={handleEditTask}
onUpdateStatus={handleUpdateStatus}
compactView={kanbanFilters.compactView}
visibleStatuses={visibleStatuses}
loading={loading}
/>
)
) : (
<KanbanBoard
tasks={filteredTasks}
onCreateTask={handleCreateTask}
onEditTask={handleEditTask}
onUpdateStatus={handleUpdateStatus}
compactView={kanbanFilters.compactView}
visibleStatuses={visibleStatuses}
/>
)}
<EditTaskForm
isOpen={!!editingTask}
onClose={() => setEditingTask(null)}
onSubmit={handleUpdateTask}
task={editingTask}
loading={loading}
/>
</>
);
}

View File

@@ -1,100 +0,0 @@
import { Task, TaskStatus } from '@/lib/types';
import { TaskCard } from './TaskCard';
import { QuickAddTask } from './QuickAddTask';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import { CreateTaskData } from '@/clients/tasks-client';
import { useState } from 'react';
import { useDroppable } from '@dnd-kit/core';
import { getStatusConfig, getTechStyle, getBadgeVariant } from '@/lib/status-config';
interface KanbanColumnProps {
id: TaskStatus;
tasks: Task[];
onCreateTask?: (data: CreateTaskData) => Promise<void>;
onEditTask?: (task: Task) => void;
compactView?: boolean;
}
export function KanbanColumn({ id, tasks, onCreateTask, onEditTask, compactView = false }: KanbanColumnProps) {
const [showQuickAdd, setShowQuickAdd] = useState(false);
// Configuration de la zone droppable
const { setNodeRef, isOver } = useDroppable({
id: id,
});
// Récupération de la config du statut
const statusConfig = getStatusConfig(id);
const style = getTechStyle(statusConfig.color);
const badgeVariant = getBadgeVariant(statusConfig.color);
return (
<div className="flex-shrink-0 w-80 md:w-1/4 md:flex-1 h-full">
<Card
ref={setNodeRef}
variant="column"
className={`h-full flex flex-col transition-all duration-200 ${
isOver ? 'ring-2 ring-[var(--primary)]/50 bg-[var(--card-hover)]' : ''
}`}
>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`w-2 h-2 rounded-full ${style.accent.replace('text-', 'bg-')} animate-pulse`}></div>
<h3 className={`font-mono text-sm font-bold ${style.accent} uppercase tracking-wider`}>
{statusConfig.label} {statusConfig.icon}
</h3>
</div>
<div className="flex items-center gap-2">
<Badge variant={badgeVariant} size="sm">
{String(tasks.length).padStart(2, '0')}
</Badge>
{onCreateTask && (
<button
onClick={() => setShowQuickAdd(true)}
className={`w-5 h-5 rounded-full border border-dashed ${style.border} ${style.accent} hover:bg-[var(--card-hover)] transition-colors flex items-center justify-center text-xs font-mono`}
title="Ajouter une tâche rapide"
>
+
</button>
)}
</div>
</div>
</CardHeader>
<CardContent className="flex-1 p-4 h-[calc(100vh-220px)] overflow-y-auto">
<div className="space-y-3">
{/* Quick Add Task */}
{showQuickAdd && onCreateTask && (
<QuickAddTask
status={id}
onSubmit={async (data) => {
await onCreateTask(data);
// Ne pas fermer automatiquement pour permettre la création en série
}}
onCancel={() => setShowQuickAdd(false)}
/>
)}
{tasks.length === 0 && !showQuickAdd ? (
<div className="text-center py-20">
<div className={`w-16 h-16 mx-auto mb-4 rounded-full bg-[var(--card)] border-2 border-dashed ${style.border} flex items-center justify-center`}>
<span className={`text-2xl ${style.accent} opacity-50`}>{statusConfig.icon}</span>
</div>
<p className="text-xs font-mono text-[var(--muted-foreground)] uppercase tracking-wide">NO DATA</p>
<div className="mt-2 flex justify-center">
<div className={`w-8 h-0.5 ${style.accent.replace('text-', 'bg-')} opacity-30`}></div>
</div>
</div>
) : (
tasks.map((task) => (
<TaskCard key={task.id} task={task} onEdit={onEditTask} compactView={compactView} />
))
)}
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,101 +0,0 @@
'use client';
import { useMemo } from 'react';
import { useTasksContext } from '@/contexts/TasksContext';
import { KanbanFilters } from './KanbanFilters';
interface JiraQuickFilterProps {
filters: KanbanFilters;
onFiltersChange: (filters: KanbanFilters) => void;
}
export function JiraQuickFilter({ filters, onFiltersChange }: JiraQuickFilterProps) {
const { regularTasks } = useTasksContext();
// Vérifier s'il y a des tâches Jira dans le système
const hasJiraTasks = useMemo(() => {
return regularTasks.some(task => task.source === 'jira');
}, [regularTasks]);
// Si pas de tâches Jira, on n'affiche rien
if (!hasJiraTasks) {
return null;
}
// Déterminer l'état actuel
const currentMode = filters.showJiraOnly ? 'show' : filters.hideJiraTasks ? 'hide' : 'all';
const handleJiraCycle = () => {
const updates: Partial<KanbanFilters> = {};
// Cycle : All -> Jira only -> No Jira -> All
switch (currentMode) {
case 'all':
// All -> Jira only
updates.showJiraOnly = true;
updates.hideJiraTasks = false;
break;
case 'show':
// Jira only -> No Jira
updates.showJiraOnly = false;
updates.hideJiraTasks = true;
break;
case 'hide':
// No Jira -> All
updates.showJiraOnly = false;
updates.hideJiraTasks = false;
break;
}
onFiltersChange({ ...filters, ...updates });
};
// Définir l'apparence selon l'état
const getButtonStyle = () => {
switch (currentMode) {
case 'show':
return 'bg-[var(--primary)]/20 text-[var(--primary)] border border-[var(--primary)]/30';
case 'hide':
return 'bg-red-500/20 text-red-400 border border-red-400/30';
default:
return 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)] hover:border-[var(--primary)]/50';
}
};
const getButtonContent = () => {
switch (currentMode) {
case 'show':
return { icon: '🔹', text: 'Jira only' };
case 'hide':
return { icon: '🚫', text: 'No Jira' };
default:
return { icon: '🔌', text: 'All tasks' };
}
};
const getTooltip = () => {
switch (currentMode) {
case 'all':
return 'Cliquer pour afficher seulement Jira';
case 'show':
return 'Cliquer pour masquer Jira';
case 'hide':
return 'Cliquer pour afficher tout';
default:
return 'Filtrer les tâches Jira';
}
};
const content = getButtonContent();
return (
<button
onClick={handleJiraCycle}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-mono transition-all ${getButtonStyle()}`}
title={getTooltip()}
>
<span>{content.icon}</span>
{content.text}
</button>
);
}

View File

@@ -1,634 +0,0 @@
'use client';
import { useState, useEffect, useRef, useMemo } from 'react';
import { createPortal } from 'react-dom';
import { TaskPriority, TaskStatus } from '@/lib/types';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { useTasksContext } from '@/contexts/TasksContext';
import { getAllPriorities, getPriorityColorHex } from '@/lib/status-config';
import { SORT_OPTIONS } from '@/lib/sort-config';
import { useUserPreferences } from '@/contexts/UserPreferencesContext';
import { ColumnVisibilityToggle } from './ColumnVisibilityToggle';
export interface KanbanFilters {
search?: string;
tags?: string[];
priorities?: TaskPriority[];
showCompleted?: boolean;
compactView?: boolean;
swimlanesByTags?: boolean;
swimlanesMode?: 'tags' | 'priority'; // Mode des swimlanes
pinnedTag?: string; // Tag pour les objectifs principaux
sortBy?: string; // Clé de l'option de tri sélectionnée
// Filtres spécifiques Jira
showJiraOnly?: boolean; // Afficher seulement les tâches Jira
hideJiraTasks?: boolean; // Masquer toutes les tâches Jira
jiraProjects?: string[]; // Filtrer par projet Jira
jiraTypes?: string[]; // Filtrer par type Jira (Story, Task, Bug, etc.)
}
interface KanbanFiltersProps {
filters: KanbanFilters;
onFiltersChange: (filters: KanbanFilters) => void;
hiddenStatuses?: Set<TaskStatus>;
onToggleStatusVisibility?: (status: TaskStatus) => void;
}
export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsHiddenStatuses, onToggleStatusVisibility }: KanbanFiltersProps) {
const { tags: availableTags, regularTasks, activeFiltersCount } = useTasksContext();
const { preferences, toggleColumnVisibility } = useUserPreferences();
// Utiliser les props si disponibles, sinon utiliser le context
const hiddenStatuses = propsHiddenStatuses || new Set(preferences.columnVisibility.hiddenStatuses);
const toggleStatusVisibility = onToggleStatusVisibility || toggleColumnVisibility;
const [isSortExpanded, setIsSortExpanded] = useState(false);
const [isSwimlaneModeExpanded, setIsSwimlaneModeExpanded] = useState(false);
const sortDropdownRef = useRef<HTMLDivElement>(null);
const swimlaneModeDropdownRef = useRef<HTMLDivElement>(null);
const sortButtonRef = useRef<HTMLButtonElement>(null);
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 });
// Fermer les dropdowns en cliquant à l'extérieur
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (sortDropdownRef.current && !sortDropdownRef.current.contains(event.target as Node)) {
setIsSortExpanded(false);
}
if (swimlaneModeDropdownRef.current && !swimlaneModeDropdownRef.current.contains(event.target as Node)) {
setIsSwimlaneModeExpanded(false);
}
}
if (isSortExpanded || isSwimlaneModeExpanded) {
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}
}, [isSortExpanded, isSwimlaneModeExpanded]);
const handleSearchChange = (search: string) => {
onFiltersChange({ ...filters, search: search || undefined });
};
const handleTagToggle = (tagName: string) => {
const currentTags = filters.tags || [];
const newTags = currentTags.includes(tagName)
? currentTags.filter(t => t !== tagName)
: [...currentTags, tagName];
onFiltersChange({
...filters,
tags: newTags
});
};
const handlePriorityToggle = (priority: TaskPriority) => {
const currentPriorities = filters.priorities || [];
const newPriorities = currentPriorities.includes(priority)
? currentPriorities.filter(p => p !== priority)
: [...currentPriorities, priority];
onFiltersChange({
...filters,
priorities: newPriorities
});
};
const handleSwimlanesToggle = () => {
onFiltersChange({
...filters,
swimlanesByTags: !filters.swimlanesByTags
});
};
const handleSwimlaneModeChange = (mode: 'tags' | 'priority') => {
onFiltersChange({
...filters,
swimlanesByTags: true,
swimlanesMode: mode
});
setIsSwimlaneModeExpanded(false);
};
const handleSwimlaneModeToggle = (event: React.MouseEvent<HTMLButtonElement>) => {
const button = event.currentTarget;
const rect = button.getBoundingClientRect();
setDropdownPosition({
top: rect.bottom + window.scrollY + 4,
left: rect.left + window.scrollX
});
setIsSwimlaneModeExpanded(!isSwimlaneModeExpanded);
};
const handleSortChange = (sortKey: string) => {
onFiltersChange({
...filters,
sortBy: sortKey
});
};
const handleSortToggle = () => {
if (!isSortExpanded && sortButtonRef.current) {
const rect = sortButtonRef.current.getBoundingClientRect();
setDropdownPosition({
top: rect.bottom + window.scrollY + 4,
left: rect.left + window.scrollX
});
}
setIsSortExpanded(!isSortExpanded);
};
const handleJiraToggle = (mode: 'show' | 'hide' | 'all') => {
const updates: Partial<KanbanFilters> = {};
switch (mode) {
case 'show':
updates.showJiraOnly = true;
updates.hideJiraTasks = false;
break;
case 'hide':
updates.showJiraOnly = false;
updates.hideJiraTasks = true;
break;
case 'all':
updates.showJiraOnly = false;
updates.hideJiraTasks = false;
break;
}
onFiltersChange({ ...filters, ...updates });
};
const handleJiraProjectToggle = (project: string) => {
const currentProjects = filters.jiraProjects || [];
const newProjects = currentProjects.includes(project)
? currentProjects.filter(p => p !== project)
: [...currentProjects, project];
onFiltersChange({
...filters,
jiraProjects: newProjects
});
};
const handleJiraTypeToggle = (type: string) => {
const currentTypes = filters.jiraTypes || [];
const newTypes = currentTypes.includes(type)
? currentTypes.filter(t => t !== type)
: [...currentTypes, type];
onFiltersChange({
...filters,
jiraTypes: newTypes
});
};
const handleClearFilters = () => {
onFiltersChange({});
};
// Récupérer les projets et types Jira disponibles dans TOUTES les tâches (pas seulement les filtrées)
// regularTasks est déjà disponible depuis la ligne 39
const availableJiraProjects = useMemo(() => {
const projects = new Set<string>();
regularTasks.forEach(task => {
if (task.source === 'jira' && task.jiraProject) {
projects.add(task.jiraProject);
}
});
return Array.from(projects).sort();
}, [regularTasks]);
const availableJiraTypes = useMemo(() => {
const types = new Set<string>();
regularTasks.forEach(task => {
if (task.source === 'jira' && task.jiraType) {
types.add(task.jiraType);
}
});
return Array.from(types).sort();
}, [regularTasks]);
// Vérifier s'il y a des tâches Jira dans le système (même masquées)
const hasJiraTasks = regularTasks.some(task => task.source === 'jira');
// Calculer les compteurs pour les priorités
const priorityCounts = useMemo(() => {
const counts: Record<string, number> = {};
getAllPriorities().forEach(priority => {
counts[priority.key] = regularTasks.filter(task => task.priority === priority.key).length;
});
return counts;
}, [regularTasks]);
// Calculer les compteurs pour les tags
const tagCounts = useMemo(() => {
const counts: Record<string, number> = {};
availableTags.forEach(tag => {
counts[tag.name] = regularTasks.filter(task => task.tags?.includes(tag.name)).length;
});
return counts;
}, [regularTasks, availableTags]);
const priorityOptions = getAllPriorities().map(priorityConfig => ({
value: priorityConfig.key,
label: priorityConfig.label,
color: priorityConfig.color,
count: priorityCounts[priorityConfig.key] || 0
}));
// Trier les tags par nombre d'utilisation (décroissant)
const sortedTags = useMemo(() => {
return [...availableTags].sort((a, b) => {
const countA = tagCounts[a.name] || 0;
const countB = tagCounts[b.name] || 0;
return countB - countA; // Décroissant
});
}, [availableTags, tagCounts]);
return (
<div className="bg-[var(--card)]/50 border-b border-[var(--border)]/50 backdrop-blur-sm">
<div className="container mx-auto px-6 py-4">
{/* Header avec recherche et bouton expand */}
<div className="flex items-center gap-4">
<div className="flex-1 max-w-md">
<Input
type="text"
value={filters.search || ''}
onChange={(e) => handleSearchChange(e.target.value)}
placeholder="Rechercher des tâches..."
className="bg-[var(--card)] border-[var(--border)]"
/>
</div>
{/* Menu swimlanes */}
<div className="flex gap-1">
<Button
variant={filters.swimlanesByTags ? "primary" : "ghost"}
onClick={handleSwimlanesToggle}
className="flex items-center gap-2"
title="Mode d'affichage"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
{filters.swimlanesByTags ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
)}
</svg>
{!filters.swimlanesByTags
? 'Normal'
: filters.swimlanesMode === 'priority'
? 'Par priorité'
: 'Par tags'
}
</Button>
{/* Bouton pour changer le mode des swimlanes */}
{filters.swimlanesByTags && (
<Button
variant="ghost"
onClick={handleSwimlaneModeToggle}
className="flex items-center gap-1 px-2"
>
<svg
className={`w-3 h-3 transition-transform ${isSwimlaneModeExpanded ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</Button>
)}
</div>
{/* Bouton de tri */}
<div className="relative" ref={sortDropdownRef}>
<Button
ref={sortButtonRef}
variant="ghost"
onClick={handleSortToggle}
className="flex items-center gap-2"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12" />
</svg>
Tris
<svg
className={`w-4 h-4 transition-transform ${isSortExpanded ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</Button>
</div>
{activeFiltersCount > 0 && (
<Button
variant="ghost"
onClick={handleClearFilters}
className="text-[var(--muted-foreground)] hover:text-[var(--destructive)]"
>
Effacer
</Button>
)}
</div>
{/* Filtres étendus */}
<div className="mt-4 border-t border-[var(--border)]/50 pt-4">
{/* Grille responsive pour les filtres principaux */}
<div className="grid grid-cols-1 lg:grid-cols-[auto_1fr] gap-6 lg:gap-8">
{/* Filtres par priorité */}
<div className="space-y-3">
<label className="block text-xs font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
Priorités
</label>
<div className="grid grid-cols-2 gap-2">
{priorityOptions.filter(priority => priority.count > 0).map((priority) => (
<button
key={priority.value}
onClick={() => handlePriorityToggle(priority.value)}
className={`flex items-center gap-2 px-3 py-2 rounded-lg border transition-all text-xs font-medium whitespace-nowrap ${
filters.priorities?.includes(priority.value)
? 'border-cyan-400 bg-cyan-400/10 text-cyan-400'
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
}`}
>
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: getPriorityColorHex(priority.color) }}
/>
{priority.label} ({priority.count})
</button>
))}
</div>
</div>
{/* Filtres par tags */}
{availableTags.length > 0 && (
<div className="space-y-3">
<label className="block text-xs font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
Tags
</label>
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto">
{sortedTags.filter(tag => (tagCounts[tag.name] || 0) > 0).map((tag) => (
<button
key={tag.id}
onClick={() => handleTagToggle(tag.name)}
className={`flex items-center gap-2 px-3 py-2 rounded-lg border transition-all text-xs font-medium ${
filters.tags?.includes(tag.name)
? 'border-cyan-400 bg-cyan-400/10 text-cyan-400'
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
}`}
>
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: tag.color }}
/>
{tag.name} ({tagCounts[tag.name]})
</button>
))}
</div>
</div>
)}
</div>
{/* Filtres Jira - Ligne séparée mais intégrée */}
{hasJiraTasks && (
<div className="border-t border-[var(--border)]/30 pt-4 mt-4">
<div className="flex items-center gap-4 mb-3">
<label className="block text-xs font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
🔌 Jira
</label>
{/* Toggle Jira Show/Hide - inline avec le titre */}
<div className="flex gap-1">
<Button
variant={filters.showJiraOnly ? "primary" : "ghost"}
onClick={() => handleJiraToggle('show')}
size="sm"
className="text-xs px-2 py-1 h-auto"
>
🔹 Seul
</Button>
<Button
variant={filters.hideJiraTasks ? "danger" : "ghost"}
onClick={() => handleJiraToggle('hide')}
size="sm"
className="text-xs px-2 py-1 h-auto"
>
🚫 Mask
</Button>
<Button
variant={(!filters.showJiraOnly && !filters.hideJiraTasks) ? "primary" : "ghost"}
onClick={() => handleJiraToggle('all')}
size="sm"
className="text-xs px-2 py-1 h-auto"
>
📋 All
</Button>
</div>
</div>
{/* Projets et Types en 2 colonnes */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Projets Jira */}
{availableJiraProjects.length > 0 && (
<div>
<label className="block text-xs font-medium text-[var(--muted-foreground)] mb-2">
Projets
</label>
<div className="flex flex-wrap gap-1">
{availableJiraProjects.map((project) => (
<button
key={project}
onClick={() => handleJiraProjectToggle(project)}
className={`px-2 py-1 rounded border transition-all text-xs font-medium ${
filters.jiraProjects?.includes(project)
? 'border-blue-400 bg-blue-400/10 text-blue-400'
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
}`}
>
📋 {project}
</button>
))}
</div>
</div>
)}
{/* Types Jira */}
{availableJiraTypes.length > 0 && (
<div>
<label className="block text-xs font-medium text-[var(--muted-foreground)] mb-2">
Types
</label>
<div className="flex flex-wrap gap-1">
{availableJiraTypes.map((type) => (
<button
key={type}
onClick={() => handleJiraTypeToggle(type)}
className={`px-2 py-1 rounded border transition-all text-xs font-medium ${
filters.jiraTypes?.includes(type)
? 'border-purple-400 bg-purple-400/10 text-purple-400'
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
}`}
>
{type === 'Feature' && '✨ '}
{type === 'Story' && '📖 '}
{type === 'Task' && '📝 '}
{type === 'Bug' && '🐛 '}
{type === 'Support' && '🛠️ '}
{type === 'Enabler' && '🔧 '}
{type}
</button>
))}
</div>
</div>
)}
</div>
</div>
)}
{/* Visibilité des colonnes */}
<div className="col-span-full border-t border-[var(--border)]/50 pt-6 mt-4">
<ColumnVisibilityToggle
hiddenStatuses={hiddenStatuses}
onToggleStatus={toggleStatusVisibility}
tasks={regularTasks}
className="text-xs"
/>
</div>
{/* Résumé des filtres actifs */}
{activeFiltersCount > 0 && (
<div className="bg-[var(--card)]/30 rounded-lg p-3 border border-[var(--border)]/50 mt-4">
<div className="text-xs text-[var(--muted-foreground)] font-mono uppercase tracking-wider mb-2">
Filtres actifs
</div>
<div className="space-y-1 text-xs">
{filters.search && (
<div className="text-[var(--muted-foreground)]">
Recherche: <span className="text-cyan-400">&ldquo;{filters.search}&rdquo;</span>
</div>
)}
{(filters.priorities?.filter(Boolean).length || 0) > 0 && (
<div className="text-[var(--muted-foreground)]">
Priorités: <span className="text-cyan-400">{filters.priorities?.filter(Boolean).join(', ')}</span>
</div>
)}
{(filters.tags?.filter(Boolean).length || 0) > 0 && (
<div className="text-[var(--muted-foreground)]">
Tags: <span className="text-cyan-400">{filters.tags?.filter(Boolean).join(', ')}</span>
</div>
)}
{filters.showJiraOnly && (
<div className="text-[var(--muted-foreground)]">
Affichage: <span className="text-blue-400">Jira seulement</span>
</div>
)}
{filters.hideJiraTasks && (
<div className="text-[var(--muted-foreground)]">
Affichage: <span className="text-red-400">Masquer Jira</span>
</div>
)}
{(filters.jiraProjects?.filter(Boolean).length || 0) > 0 && (
<div className="text-[var(--muted-foreground)]">
Projets Jira: <span className="text-blue-400">{filters.jiraProjects?.filter(Boolean).join(', ')}</span>
</div>
)}
{(filters.jiraTypes?.filter(Boolean).length || 0) > 0 && (
<div className="text-[var(--muted-foreground)]">
Types Jira: <span className="text-purple-400">{filters.jiraTypes?.filter(Boolean).join(', ')}</span>
</div>
)}
</div>
</div>
)}
</div>
</div>
{/* Dropdown de tri rendu via portail pour éviter les problèmes de z-index */}
{isSortExpanded && typeof window !== 'undefined' && createPortal(
<div
ref={sortDropdownRef}
className="fixed w-80 bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-xl z-[9999] max-h-64 overflow-y-auto"
style={{
top: dropdownPosition.top,
left: dropdownPosition.left
}}
>
{SORT_OPTIONS.map((option) => (
<button
key={option.key}
onClick={() => {
handleSortChange(option.key);
setIsSortExpanded(false);
}}
className={`w-full px-3 py-2 text-left text-xs font-mono hover:bg-[var(--card-hover)] transition-colors flex items-center gap-2 ${
(filters.sortBy || 'priority-desc') === option.key
? 'bg-cyan-600/20 text-cyan-400 border-l-2 border-cyan-400'
: 'text-[var(--muted-foreground)]'
}`}
>
<span className="text-base">{option.icon}</span>
<span className="flex-1">{option.label}</span>
{(filters.sortBy || 'priority-desc') === option.key && (
<svg className="w-4 h-4 text-cyan-400" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
)}
</button>
))}
</div>,
document.body
)}
{/* Dropdown des modes swimlanes rendu via portail pour éviter les problèmes de z-index */}
{isSwimlaneModeExpanded && typeof window !== 'undefined' && createPortal(
<div
ref={swimlaneModeDropdownRef}
className="fixed bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-xl z-[9999] min-w-[140px]"
style={{
top: dropdownPosition.top,
left: dropdownPosition.left,
}}
>
<button
onClick={() => handleSwimlaneModeChange('tags')}
className={`w-full px-3 py-2 text-left text-xs hover:bg-[var(--card-hover)] transition-colors flex items-center gap-2 first:rounded-t-lg ${
(!filters.swimlanesMode || filters.swimlanesMode === 'tags') ? 'bg-[var(--card-hover)] text-[var(--primary)]' : 'text-[var(--muted-foreground)]'
}`}
>
🏷 Par tags
</button>
<button
onClick={() => handleSwimlaneModeChange('priority')}
className={`w-full px-3 py-2 text-left text-xs hover:bg-[var(--card-hover)] transition-colors flex items-center gap-2 last:rounded-b-lg ${
filters.swimlanesMode === 'priority' ? 'bg-[var(--card-hover)] text-[var(--primary)]' : 'text-[var(--muted-foreground)]'
}`}
>
🎯 Par priorité
</button>
</div>,
document.body
)}
</div>
);
}

View File

@@ -1,262 +0,0 @@
'use client';
import { useState } from 'react';
import { useUserPreferences } from '@/contexts/UserPreferencesContext';
import { useDragAndDrop } from '@/hooks/useDragAndDrop';
import { Task, TaskStatus } from '@/lib/types';
import { TaskCard } from './TaskCard';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import {
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent
} from '@dnd-kit/core';
import {
SortableContext,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { useDroppable } from '@dnd-kit/core';
interface ObjectivesBoardProps {
tasks: Task[];
onEditTask?: (task: Task) => void;
onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise<void>;
compactView?: boolean;
pinnedTagName?: string;
}
// Composant pour les colonnes droppables
function DroppableColumn({
status,
tasks,
title,
color,
icon,
onEditTask,
compactView
}: {
status: TaskStatus;
tasks: Task[];
title: string;
color: string;
icon: string;
onEditTask?: (task: Task) => void;
compactView: boolean;
}) {
const { setNodeRef } = useDroppable({
id: status,
});
return (
<div ref={setNodeRef} className="space-y-3">
<div className="flex items-center gap-2 pt-2 pb-2 border-b border-[var(--accent)]/20">
<div className={`w-2 h-2 rounded-full ${color}`}></div>
<h3 className={`text-sm font-mono font-medium uppercase tracking-wider ${color.replace('bg-', 'text-').replace('400', '300')}`}>
{title}
</h3>
<div className="flex-1"></div>
<span className="text-xs text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
{tasks.length}
</span>
</div>
{tasks.length === 0 ? (
<div className="text-center py-8 text-[var(--muted-foreground)] text-sm">
<div className="text-2xl mb-2">{icon}</div>
Aucun objectif {title.toLowerCase()}
</div>
) : (
<SortableContext items={tasks.map(t => t.id)} strategy={verticalListSortingStrategy}>
<div className="space-y-3">
{tasks.map(task => (
<div key={task.id} className="transform hover:scale-[1.02] transition-transform duration-200">
<TaskCard
task={task}
onEdit={onEditTask}
compactView={compactView}
/>
</div>
))}
</div>
</SortableContext>
)}
</div>
);
}
export function ObjectivesBoard({
tasks,
onEditTask,
onUpdateStatus,
compactView = false,
pinnedTagName = "Objectifs"
}: ObjectivesBoardProps) {
const { preferences, toggleObjectivesCollapse } = useUserPreferences();
const isCollapsed = preferences.viewPreferences.objectivesCollapsed;
const { isMounted, sensors } = useDragAndDrop();
const [activeTask, setActiveTask] = useState<Task | null>(null);
// Handlers pour le drag & drop
const handleDragStart = (event: DragStartEvent) => {
const task = tasks.find(t => t.id === event.active.id);
setActiveTask(task || null);
};
const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;
setActiveTask(null);
if (!over || !onUpdateStatus) return;
const taskId = active.id as string;
const newStatus = over.id as TaskStatus;
// Vérifier si le statut a changé
const task = tasks.find(t => t.id === taskId);
if (task && task.status !== newStatus) {
await onUpdateStatus(taskId, newStatus);
}
};
if (tasks.length === 0) {
return null; // Ne rien afficher s'il n'y a pas d'objectifs
}
const content = (
<div className="bg-[var(--card)]/30 border-b border-[var(--accent)]/30">
<div className="container mx-auto px-6 py-4">
<Card variant="column" className="border-[var(--accent)]/30 shadow-[var(--accent)]/10">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<button
onClick={toggleObjectivesCollapse}
className="flex items-center gap-3 hover:bg-[var(--accent)]/20 rounded-lg p-2 -m-2 transition-colors group"
>
<div className="w-3 h-3 bg-[var(--accent)] rounded-full animate-pulse shadow-[var(--accent)]/50 shadow-lg"></div>
<h2 className="text-lg font-mono font-bold text-[var(--accent)] uppercase tracking-wider">
🎯 Objectifs Principaux
</h2>
{pinnedTagName && (
<Badge variant="outline" className="border-[var(--accent)]/50 text-[var(--accent)]">
{pinnedTagName}
</Badge>
)}
{/* Flèche de collapse */}
<svg
className={`w-4 h-4 text-[var(--accent)] transition-transform duration-200 ${
isCollapsed ? 'rotate-180' : ''
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
<div className="flex items-center gap-2">
<Badge variant="primary" size="sm" className="bg-[var(--accent)]">
{String(tasks.length).padStart(2, '0')}
</Badge>
{/* Bouton collapse séparé pour mobile */}
<button
onClick={toggleObjectivesCollapse}
className="lg:hidden p-1 hover:bg-[var(--accent)]/20 rounded transition-colors"
aria-label={isCollapsed ? "Développer" : "Réduire"}
>
<svg
className={`w-4 h-4 text-[var(--accent)] transition-transform duration-200 ${
isCollapsed ? 'rotate-180' : ''
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
</div>
</CardHeader>
{!isCollapsed && (
<CardContent className="pt-0">
{(() => {
// Séparer les tâches par statut
const inProgressTasks = tasks.filter(task => task.status === 'in_progress');
const todoTasks = tasks.filter(task => task.status === 'todo' || task.status === 'backlog');
const completedTasks = tasks.filter(task => task.status === 'done');
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<DroppableColumn
status="todo"
tasks={todoTasks}
title="À faire"
color="bg-[var(--primary)]"
icon="📋"
onEditTask={onEditTask}
compactView={compactView}
/>
<DroppableColumn
status="in_progress"
tasks={inProgressTasks}
title="En cours"
color="bg-yellow-400"
icon="🔄"
onEditTask={onEditTask}
compactView={compactView}
/>
<DroppableColumn
status="done"
tasks={completedTasks}
title="Terminé"
color="bg-green-400"
icon="✅"
onEditTask={onEditTask}
compactView={compactView}
/>
</div>
);
})()}
</CardContent>
)}
</Card>
</div>
</div>
);
if (!isMounted) {
return content;
}
return (
<DndContext
id="objectives-board"
sensors={sensors}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
{content}
{/* Overlay pour le drag & drop */}
<DragOverlay>
{activeTask ? (
<div className="rotate-3 opacity-90">
<TaskCard
task={activeTask}
onEdit={undefined}
compactView={compactView}
/>
</div>
) : null}
</DragOverlay>
</DndContext>
);
}

View File

@@ -1,213 +0,0 @@
'use client';
import { useState, useRef, useEffect } from 'react';
import { Card } from '@/components/ui/Card';
import { TagInput } from '@/components/ui/TagInput';
import { TaskStatus, TaskPriority } from '@/lib/types';
import { CreateTaskData } from '@/clients/tasks-client';
import { getAllPriorities } from '@/lib/status-config';
interface QuickAddTaskProps {
status: TaskStatus;
onSubmit: (data: CreateTaskData) => Promise<void>;
onCancel: () => void;
// Contexte pour les swimlanes
swimlaneContext?: {
type: 'tag' | 'priority';
value: string; // nom du tag ou clé de la priorité
};
}
export function QuickAddTask({ status, onSubmit, onCancel, swimlaneContext }: QuickAddTaskProps) {
// Fonction pour initialiser les données selon le contexte
const getInitialFormData = (): CreateTaskData => {
const baseData: CreateTaskData = {
title: '',
description: '',
status,
priority: 'medium' as TaskPriority,
tags: [],
dueDate: undefined
};
// Pré-remplir selon le contexte de swimlane
if (swimlaneContext) {
if (swimlaneContext.type === 'tag' && swimlaneContext.value !== 'Sans tag') {
baseData.tags = [swimlaneContext.value];
} else if (swimlaneContext.type === 'priority') {
baseData.priority = swimlaneContext.value as TaskPriority;
}
}
return baseData;
};
const [formData, setFormData] = useState<CreateTaskData>(getInitialFormData());
const [isSubmitting, setIsSubmitting] = useState(false);
const [activeField, setActiveField] = useState<'title' | 'description' | 'tags' | 'date' | null>('title');
const titleRef = useRef<HTMLInputElement>(null);
// Focus automatique sur le titre
useEffect(() => {
titleRef.current?.focus();
}, []);
const handleSubmit = async () => {
const trimmedTitle = formData.title.trim();
console.log('handleSubmit called:', { trimmedTitle, isSubmitting });
if (!trimmedTitle || isSubmitting) return;
setIsSubmitting(true);
try {
console.log('Submitting task:', { ...formData, title: trimmedTitle });
await onSubmit({
...formData,
title: trimmedTitle
});
// Réinitialiser pour la prochaine tâche (en gardant le contexte)
setFormData(getInitialFormData());
setActiveField('title');
setIsSubmitting(false);
titleRef.current?.focus();
} catch (error) {
console.error('Erreur lors de la création:', error);
setIsSubmitting(false);
}
};
const handleKeyDown = (e: React.KeyboardEvent, field: string) => {
console.log('Key pressed:', e.key, 'field:', field, 'title:', formData.title);
// Seulement intercepter les touches spécifiques qu'on veut gérer
if (e.key === 'Enter') {
e.preventDefault();
e.stopPropagation();
if (field === 'title' && formData.title.trim()) {
console.log('Calling handleSubmit from title');
handleSubmit();
} else if (field === 'tags') {
// TagInput gère ses propres événements Enter
} else if (formData.title.trim()) {
// Permettre création depuis n'importe quel champ si titre rempli
console.log('Calling handleSubmit from other field');
handleSubmit();
}
} else if (e.key === 'Escape') {
e.preventDefault();
onCancel();
} else if (e.key === 'Tab' && !e.metaKey && !e.ctrlKey) {
// Navigation entre les champs seulement si pas de modificateur
e.preventDefault();
const fields = ['title', 'description', 'tags', 'date'];
const currentIndex = fields.indexOf(activeField || 'title');
const nextField = fields[(currentIndex + 1) % fields.length] as typeof activeField;
setActiveField(nextField);
}
// Laisser passer tous les autres événements (y compris les raccourcis système)
};
const handleTagsChange = (tags: string[]) => {
setFormData(prev => ({
...prev,
tags
}));
};
const handleBlur = (e: React.FocusEvent) => {
// Vérifier si le focus reste dans le composant
setTimeout(() => {
const currentTarget = e.currentTarget;
const relatedTarget = e.relatedTarget as Node | null;
// Si le focus sort complètement du composant ET qu'il n'y a pas de titre
if (currentTarget && (!relatedTarget || !currentTarget.contains(relatedTarget)) && !formData.title.trim()) {
onCancel();
}
}, 100);
};
return (
<div onBlur={handleBlur}>
<Card className="p-3 border-dashed border-[var(--primary)]/30 bg-[var(--card)]/50 hover:border-[var(--primary)]/50 transition-all duration-300">
{/* Header avec titre et priorité */}
<div className="flex items-start gap-2 mb-2 min-w-0">
<input
ref={titleRef}
type="text"
value={formData.title}
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
onKeyDown={(e) => handleKeyDown(e, 'title')}
onFocus={() => setActiveField('title')}
placeholder="Titre de la tâche..."
disabled={isSubmitting}
className="flex-1 min-w-0 bg-transparent border-none outline-none text-[var(--foreground)] font-mono text-sm font-medium placeholder-[var(--muted-foreground)] leading-tight"
/>
{/* Indicateur de priorité */}
<select
value={formData.priority}
onChange={(e) => setFormData(prev => ({ ...prev, priority: e.target.value as TaskPriority }))}
disabled={isSubmitting}
className="flex-shrink-0 w-10 bg-transparent border-none outline-none text-lg text-[var(--muted-foreground)] cursor-pointer text-center"
title={getAllPriorities().find(p => p.key === formData.priority)?.label}
>
{getAllPriorities().map(priorityConfig => (
<option key={priorityConfig.key} value={priorityConfig.key}>
{priorityConfig.icon}
</option>
))}
</select>
</div>
{/* Description */}
<textarea
value={formData.description}
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
onKeyDown={(e) => handleKeyDown(e, 'description')}
onFocus={() => setActiveField('description')}
placeholder="Description..."
rows={2}
disabled={isSubmitting}
className="w-full bg-transparent border-none outline-none text-xs text-[var(--muted-foreground)] font-mono placeholder-[var(--muted-foreground)] resize-none mb-2"
/>
{/* Tags */}
<div className="mb-2">
<TagInput
tags={formData.tags || []}
onChange={handleTagsChange}
placeholder="Tags..."
maxTags={5}
className="text-xs"
compactSuggestions={true}
/>
</div>
{/* Footer avec date et actions */}
<div className="pt-2 border-t border-[var(--border)]/50">
<div className="flex items-center justify-between text-xs min-w-0">
<input
type="datetime-local"
value={formData.dueDate ? new Date(formData.dueDate.getTime() - formData.dueDate.getTimezoneOffset() * 60000).toISOString().slice(0, 16) : ''}
onChange={(e) => setFormData(prev => ({
...prev,
dueDate: e.target.value ? new Date(e.target.value) : undefined
}))}
onFocus={() => setActiveField('date')}
disabled={isSubmitting}
className="bg-transparent border-none outline-none text-[var(--muted-foreground)] font-mono text-xs flex-shrink min-w-0"
/>
{isSubmitting && (
<div className="flex items-center gap-1 text-[var(--primary)] font-mono text-xs flex-shrink-0">
<div className="w-3 h-3 border border-[var(--primary)] border-t-transparent rounded-full animate-spin"></div>
<span>...</span>
</div>
)}
</div>
</div>
</Card>
</div>
);
}

View File

@@ -1,487 +0,0 @@
import { useState, useEffect, useRef, useTransition } from 'react';
import { Task } from '@/lib/types';
import { formatDistanceToNow } from 'date-fns';
import { fr } from 'date-fns/locale';
import { Card } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import { TagDisplay } from '@/components/ui/TagDisplay';
import { useTasksContext } from '@/contexts/TasksContext';
import { useUserPreferences } from '@/contexts/UserPreferencesContext';
import { useDraggable } from '@dnd-kit/core';
import { getPriorityConfig, getPriorityColorHex } from '@/lib/status-config';
import { updateTaskTitle, deleteTask } from '@/actions/tasks';
interface TaskCardProps {
task: Task;
onEdit?: (task: Task) => void;
compactView?: boolean;
}
export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
const [isEditingTitle, setIsEditingTitle] = useState(false);
const [editTitle, setEditTitle] = useState(task.title);
const [showTooltip, setShowTooltip] = useState(false);
const [isPending, startTransition] = useTransition();
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const { tags: availableTags, refreshTasks } = useTasksContext();
const { preferences } = useUserPreferences();
// Classes CSS pour les différentes tailles de police
const getFontSizeClasses = () => {
switch (preferences.viewPreferences.fontSize) {
case 'small':
return {
title: 'text-xs',
description: 'text-xs',
meta: 'text-xs'
};
case 'large':
return {
title: 'text-base',
description: 'text-sm',
meta: 'text-sm'
};
default: // medium
return {
title: 'text-sm',
description: 'text-xs',
meta: 'text-xs'
};
}
};
const fontClasses = getFontSizeClasses();
// Helper pour construire l'URL Jira
const getJiraTicketUrl = (jiraKey: string): string => {
const baseUrl = preferences.jiraConfig.baseUrl;
if (!baseUrl || !jiraKey) return '';
return `${baseUrl}/browse/${jiraKey}`;
};
// Configuration du draggable
const {
attributes,
listeners,
setNodeRef,
transform,
isDragging,
} = useDraggable({
id: task.id,
});
// Mettre à jour le titre local quand la tâche change
useEffect(() => {
setEditTitle(task.title);
}, [task.title]);
// Nettoyer le timeout au démontage
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
const handleDelete = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (window.confirm('Êtes-vous sûr de vouloir supprimer cette tâche ?')) {
startTransition(async () => {
const result = await deleteTask(task.id);
if (!result.success) {
console.error('Error deleting task:', result.error);
// TODO: Afficher une notification d'erreur
} else {
// Rafraîchir les données après suppression réussie
await refreshTasks();
}
});
}
};
const handleEdit = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (onEdit) {
onEdit(task);
}
};
const handleTitleClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (!isDragging && !isPending) {
setIsEditingTitle(true);
setShowTooltip(false);
}
};
const handleTitleSave = async () => {
const trimmedTitle = editTitle.trim();
if (trimmedTitle && trimmedTitle !== task.title) {
startTransition(async () => {
const result = await updateTaskTitle(task.id, trimmedTitle);
if (!result.success) {
console.error('Error updating task title:', result.error);
// Remettre l'ancien titre en cas d'erreur
setEditTitle(task.title);
} else {
// Mettre à jour optimistiquement le titre local
// La Server Action a déjà mis à jour la DB, on synchronise juste l'affichage
task.title = trimmedTitle;
}
});
}
setIsEditingTitle(false);
setShowTooltip(false);
};
const handleTitleCancel = () => {
setEditTitle(task.title);
setIsEditingTitle(false);
setShowTooltip(false);
};
const handleTitleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
handleTitleSave();
} else if (e.key === 'Escape') {
e.preventDefault();
handleTitleCancel();
}
};
const handleMouseEnter = () => {
if (!isEditingTitle) {
timeoutRef.current = setTimeout(() => {
setShowTooltip(true);
}, 100);
}
};
const handleMouseLeave = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
setShowTooltip(false);
};
// Style de transformation pour le drag
const style = transform ? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
} : undefined;
// Extraire les emojis du titre pour les afficher comme tags visuels
const emojiRegex = /(?:[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F900}-\u{1F9FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}])(?:[\u{200D}][\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F900}-\u{1F9FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{FE0F}])*/gu;
const titleEmojis = task.title.match(emojiRegex) || [];
const titleWithoutEmojis = task.title.replace(emojiRegex, '').trim();
// Composant titre avec tooltip
const TitleWithTooltip = () => (
<div className="relative flex-1">
<h4
className={`font-mono ${fontClasses.title} font-medium text-[var(--foreground)] leading-tight line-clamp-2 cursor-pointer hover:text-[var(--primary)] transition-colors`}
onClick={handleTitleClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
title="Cliquer pour éditer"
>
{titleWithoutEmojis}
</h4>
{/* Tooltip */}
{showTooltip && (
<div className="absolute z-50 bottom-full left-0 mb-2 px-2 py-1 bg-[var(--background)] border border-[var(--border)] rounded-md shadow-lg max-w-xs whitespace-normal break-words text-xs font-mono text-[var(--foreground)]">
{titleWithoutEmojis}
<div className="absolute top-full left-2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-[var(--border)]"></div>
</div>
)}
</div>
);
// Si pas d'emoji dans le titre, utiliser l'emoji du premier tag
let displayEmojis: string[] = titleEmojis;
if (displayEmojis.length === 0 && task.tags && task.tags.length > 0) {
const firstTag = availableTags.find(tag => tag.name === task.tags[0]);
if (firstTag) {
const tagEmojis = firstTag.name.match(emojiRegex);
if (tagEmojis && tagEmojis.length > 0) {
displayEmojis = [tagEmojis[0]]; // Prendre seulement le premier emoji du tag
}
}
}
// Styles spéciaux pour les tâches Jira
const isJiraTask = task.source === 'jira';
const jiraStyles = isJiraTask ? {
border: '1px solid rgba(0, 130, 201, 0.3)',
borderLeft: '3px solid #0082C9',
background: 'linear-gradient(135deg, rgba(0, 130, 201, 0.05) 0%, rgba(0, 130, 201, 0.02) 100%)'
} : {};
// Vue compacte : seulement le titre
if (compactView) {
return (
<Card
ref={setNodeRef}
style={{ ...style, ...jiraStyles }}
className={`p-2 hover:border-[var(--primary)]/30 hover:shadow-lg hover:shadow-[var(--primary)]/10 transition-all duration-300 cursor-pointer group ${
isDragging ? 'opacity-50 rotate-3 scale-105' : ''
} ${
task.status === 'done' ? 'opacity-60' : ''
} ${
isJiraTask ? 'jira-task' : ''
} ${
isPending ? 'opacity-70 pointer-events-none' : ''
}`}
{...attributes}
{...(isEditingTitle ? {} : listeners)}
>
<div className="flex items-center gap-2">
{displayEmojis.length > 0 && (
<div className="flex gap-1 flex-shrink-0">
{displayEmojis.slice(0, 1).map((emoji, index) => (
<span
key={index}
className="text-base opacity-90 font-emoji"
style={{
fontFamily: 'Apple Color Emoji, Segoe UI Emoji, Noto Color Emoji, sans-serif',
fontVariantEmoji: 'normal'
}}
>
{emoji}
</span>
))}
</div>
)}
{isEditingTitle ? (
<input
type="text"
value={editTitle}
onChange={(e) => setEditTitle(e.target.value)}
onKeyDown={handleTitleKeyPress}
onBlur={handleTitleSave}
autoFocus
className={`flex-1 bg-transparent border-none outline-none text-[var(--foreground)] font-mono ${fontClasses.title} font-medium leading-tight`}
/>
) : (
<TitleWithTooltip />
)}
<div className="flex items-center gap-1 flex-shrink-0">
{/* Boutons d'action compacts - masqués en mode édition */}
{!isEditingTitle && onEdit && (
<button
onClick={handleEdit}
disabled={isPending}
className="opacity-0 group-hover:opacity-100 w-5 h-5 rounded-full bg-[var(--primary)]/20 hover:bg-[var(--primary)]/30 border border-[var(--primary)]/30 hover:border-[var(--primary)]/50 flex items-center justify-center transition-all duration-200 text-[var(--primary)] hover:text-[var(--primary)] text-xs disabled:opacity-50"
title="Modifier la tâche"
>
</button>
)}
{!isEditingTitle && (
<button
onClick={handleDelete}
disabled={isPending}
className="opacity-0 group-hover:opacity-100 w-5 h-5 rounded-full bg-[var(--destructive)]/20 hover:bg-[var(--destructive)]/30 border border-[var(--destructive)]/30 hover:border-[var(--destructive)]/50 flex items-center justify-center transition-all duration-200 text-[var(--destructive)] hover:text-[var(--destructive)] text-xs disabled:opacity-50"
title="Supprimer la tâche"
>
×
</button>
)}
{/* Indicateur de priorité compact */}
<div
className="w-1.5 h-1.5 rounded-full"
style={{ backgroundColor: getPriorityColorHex(getPriorityConfig(task.priority).color) }}
/>
</div>
</div>
</Card>
);
}
// Vue détaillée : version complète
return (
<Card
ref={setNodeRef}
style={{ ...style, ...jiraStyles }}
className={`p-3 hover:border-[var(--primary)]/30 hover:shadow-lg hover:shadow-[var(--primary)]/10 transition-all duration-300 cursor-pointer group ${
isDragging ? 'opacity-50 rotate-3 scale-105' : ''
} ${
task.status === 'done' ? 'opacity-60' : ''
} ${
isJiraTask ? 'jira-task' : ''
} ${
isPending ? 'opacity-70 pointer-events-none' : ''
}`}
{...attributes}
{...(isEditingTitle ? {} : listeners)}
>
{/* Header tech avec titre et status */}
<div className="flex items-start gap-2 mb-2">
{displayEmojis.length > 0 && (
<div className="flex gap-1 flex-shrink-0">
{displayEmojis.slice(0, 2).map((emoji, index) => (
<span
key={index}
className="text-sm opacity-80 font-emoji"
style={{
fontFamily: 'Apple Color Emoji, Segoe UI Emoji, Noto Color Emoji, sans-serif',
fontVariantEmoji: 'normal'
}}
>
{emoji}
</span>
))}
</div>
)}
{isEditingTitle ? (
<input
type="text"
value={editTitle}
onChange={(e) => setEditTitle(e.target.value)}
onKeyDown={handleTitleKeyPress}
onBlur={handleTitleSave}
autoFocus
className="flex-1 bg-transparent border-none outline-none text-[var(--foreground)] font-mono text-sm font-medium leading-tight"
/>
) : (
<TitleWithTooltip />
)}
<div className="flex items-center gap-1 flex-shrink-0">
{/* Bouton d'édition discret - masqué en mode édition */}
{!isEditingTitle && onEdit && (
<button
onClick={handleEdit}
disabled={isPending}
className="opacity-0 group-hover:opacity-100 w-4 h-4 rounded-full bg-[var(--primary)]/20 hover:bg-[var(--primary)]/30 border border-[var(--primary)]/30 hover:border-[var(--primary)]/50 flex items-center justify-center transition-all duration-200 text-[var(--primary)] hover:text-[var(--primary)] text-xs disabled:opacity-50"
title="Modifier la tâche"
>
</button>
)}
{/* Bouton de suppression discret - masqué en mode édition */}
{!isEditingTitle && (
<button
onClick={handleDelete}
disabled={isPending}
className="opacity-0 group-hover:opacity-100 w-4 h-4 rounded-full bg-[var(--destructive)]/20 hover:bg-[var(--destructive)]/30 border border-[var(--destructive)]/30 hover:border-[var(--destructive)]/50 flex items-center justify-center transition-all duration-200 text-[var(--destructive)] hover:text-[var(--destructive)] text-xs disabled:opacity-50"
title="Supprimer la tâche"
>
×
</button>
)}
{/* Indicateur de priorité tech */}
<div
className="w-2 h-2 rounded-full animate-pulse shadow-sm"
style={{
backgroundColor: getPriorityColorHex(getPriorityConfig(task.priority).color),
boxShadow: `0 0 4px ${getPriorityColorHex(getPriorityConfig(task.priority).color)}50`
}}
/>
</div>
</div>
{/* Description tech */}
{task.description && (
<p className={`${fontClasses.description} text-[var(--muted-foreground)] mb-3 line-clamp-1 font-mono`}>
{task.description}
</p>
)}
{/* Tags avec couleurs */}
{task.tags && task.tags.length > 0 && (
<div className={
(task.dueDate || (task.source && task.source !== 'manual') || task.completedAt)
? "mb-3"
: "mb-0"
}>
<TagDisplay
tags={task.tags}
availableTags={availableTags}
size="sm"
maxTags={3}
showColors={true}
/>
</div>
)}
{/* Footer tech avec séparateur néon - seulement si des données à afficher */}
{(task.dueDate || (task.source && task.source !== 'manual') || task.completedAt) && (
<div className="pt-2 border-t border-[var(--border)]/50">
<div className={`flex items-center justify-between ${fontClasses.meta}`}>
{task.dueDate ? (
<span className="flex items-center gap-1 text-[var(--muted-foreground)] font-mono">
<span className="text-[var(--primary)]"></span>
{formatDistanceToNow(new Date(task.dueDate), {
addSuffix: true,
locale: fr
})}
</span>
) : (
<div></div>
)}
<div className="flex items-center gap-2">
{task.source !== 'manual' && task.source && (
task.source === 'jira' && task.jiraKey ? (
preferences.jiraConfig.baseUrl ? (
<a
href={getJiraTicketUrl(task.jiraKey)}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="hover:scale-105 transition-transform"
>
<Badge variant="outline" size="sm" className="hover:bg-blue-500/10 hover:border-blue-400/50 cursor-pointer">
{task.jiraKey}
</Badge>
</a>
) : (
<Badge variant="outline" size="sm">
{task.jiraKey}
</Badge>
)
) : (
<Badge variant="outline" size="sm">
{task.source}
</Badge>
)
)}
{task.jiraProject && (
<Badge variant="outline" size="sm" className="text-blue-400 border-blue-400/30">
{task.jiraProject}
</Badge>
)}
{task.jiraType && (
<Badge variant="outline" size="sm" className="text-purple-400 border-purple-400/30">
{task.jiraType}
</Badge>
)}
{task.completedAt && (
<span className="text-emerald-400 font-mono font-bold"> DONE</span>
)}
</div>
</div>
</div>
)}
</Card>
);
}

View File

@@ -1,260 +0,0 @@
'use client';
import { useState } from 'react';
import { UserPreferences } from '@/lib/types';
import { Header } from '@/components/ui/Header';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { UserPreferencesProvider } from '@/contexts/UserPreferencesContext';
import { backupClient, BackupListResponse } from '@/clients/backup-client';
import Link from 'next/link';
interface DatabaseStats {
taskCount: number;
tagCount: number;
completionRate: number;
}
interface AdvancedSettingsPageClientProps {
initialPreferences: UserPreferences;
initialDbStats: DatabaseStats;
initialBackupData: BackupListResponse;
}
export function AdvancedSettingsPageClient({
initialPreferences,
initialDbStats,
initialBackupData
}: AdvancedSettingsPageClientProps) {
const [backupData, setBackupData] = useState<BackupListResponse>(initialBackupData);
const [dbStats] = useState<DatabaseStats>(initialDbStats);
const [isCreatingBackup, setIsCreatingBackup] = useState(false);
const [isVerifying, setIsVerifying] = useState(false);
const reloadBackupData = async () => {
try {
const data = await backupClient.listBackups();
setBackupData(data);
} catch (error) {
console.error('Failed to reload backup data:', error);
}
};
const handleCreateBackup = async () => {
setIsCreatingBackup(true);
try {
await backupClient.createBackup();
await reloadBackupData();
} catch (error) {
console.error('Failed to create backup:', error);
} finally {
setIsCreatingBackup(false);
}
};
const handleVerifyDatabase = async () => {
setIsVerifying(true);
try {
await backupClient.verifyDatabase();
alert('✅ Base de données vérifiée avec succès');
} catch (error) {
console.error('Database verification failed:', error);
alert('❌ Erreur lors de la vérification de la base');
} finally {
setIsVerifying(false);
}
};
const formatFileSize = (bytes: number): string => {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(1)} ${units[unitIndex]}`;
};
const formatTimeAgo = (date: Date): string => {
const now = new Date();
const diffMs = now.getTime() - new Date(date).getTime();
const diffMins = Math.floor(diffMs / (1000 * 60));
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffMins < 60) {
return `il y a ${diffMins}min`;
} else if (diffHours < 24) {
return `il y a ${diffHours}h ${diffMins % 60}min`;
} else {
return `il y a ${diffDays}j`;
}
};
const getNextBackupTime = (): string => {
if (!backupData.scheduler.nextBackup) return 'Non planifiée';
const nextBackup = new Date(backupData.scheduler.nextBackup);
const now = new Date();
const diffMs = nextBackup.getTime() - now.getTime();
const diffMins = Math.floor(diffMs / (1000 * 60));
const diffHours = Math.floor(diffMins / 60);
if (diffMins < 60) {
return `dans ${diffMins}min`;
} else {
return `dans ${diffHours}h ${diffMins % 60}min`;
}
};
return (
<UserPreferencesProvider initialPreferences={initialPreferences}>
<div className="min-h-screen bg-[var(--background)]">
<Header
title="TowerControl"
subtitle="Paramètres avancés"
/>
<div className="container mx-auto px-4 py-4">
<div className="max-w-4xl mx-auto">
{/* Breadcrumb */}
<div className="mb-4 text-sm">
<Link href="/settings" className="text-[var(--muted-foreground)] hover:text-[var(--primary)]">
Paramètres
</Link>
<span className="mx-2 text-[var(--muted-foreground)]">/</span>
<span className="text-[var(--foreground)]">Avancé</span>
</div>
{/* Page Header */}
<div className="mb-6">
<h1 className="text-2xl font-mono font-bold text-[var(--foreground)] mb-2">
🛠 Paramètres avancés
</h1>
<p className="text-[var(--muted-foreground)]">
Configuration système, sauvegarde et outils de développement
</p>
</div>
<div className="space-y-6">
{/* Sauvegarde et données */}
<Card>
<CardHeader>
<h2 className="text-lg font-semibold">💾 Sauvegarde et données</h2>
<p className="text-sm text-[var(--muted-foreground)]">
Gestion des sauvegardes automatiques et manuelles
</p>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
<div className="flex items-center justify-between mb-2">
<h3 className="font-medium">Sauvegarde automatique</h3>
<span className={`h-2 w-2 rounded-full ${
backupData.scheduler.isRunning ? 'bg-green-500' : 'bg-red-500'
}`}></span>
</div>
<p className="text-sm text-[var(--muted-foreground)] mb-2">
{backupData.scheduler.isEnabled
? `Sauvegarde ${backupData.scheduler.interval === 'hourly' ? 'toutes les heures' :
backupData.scheduler.interval === 'daily' ? 'quotidienne' : 'hebdomadaire'}`
: 'Sauvegarde automatique désactivée'
}
</p>
<p className="text-xs text-[var(--muted-foreground)]">
{backupData.scheduler.isRunning
? `Prochaine sauvegarde: ${getNextBackupTime()}`
: 'Planificateur arrêté'
}
</p>
</div>
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
<h3 className="font-medium mb-2">Sauvegardes disponibles</h3>
<p className="text-sm text-[var(--muted-foreground)] mb-2">
{backupData.backups.length} sauvegarde{backupData.backups.length > 1 ? 's' : ''} conservée{backupData.backups.length > 1 ? 's' : ''}
</p>
{backupData.backups.length > 0 ? (
<p className="text-xs text-[var(--muted-foreground)]">
Dernière: {formatTimeAgo(backupData.backups[0].createdAt)}
({formatFileSize(backupData.backups[0].size)})
</p>
) : (
<p className="text-xs text-[var(--muted-foreground)]">
Aucune sauvegarde disponible
</p>
)}
</div>
</div>
<div className="flex gap-2 pt-2">
<Button
onClick={handleCreateBackup}
disabled={isCreatingBackup}
className="px-3 py-1.5 bg-[var(--primary)] text-[var(--primary-foreground)] rounded text-sm font-medium"
>
{isCreatingBackup ? 'Création...' : 'Créer une sauvegarde'}
</Button>
<Link href="/settings/backup">
<Button className="px-3 py-1.5 bg-[var(--card)] text-[var(--foreground)] border border-[var(--border)] rounded text-sm font-medium">
Gérer les sauvegardes
</Button>
</Link>
<Button
onClick={handleVerifyDatabase}
disabled={isVerifying}
className="px-3 py-1.5 bg-[var(--card)] text-[var(--foreground)] border border-[var(--border)] rounded text-sm font-medium"
>
{isVerifying ? 'Vérification...' : 'Vérifier DB'}
</Button>
</div>
</CardContent>
</Card>
{/* Base de données */}
<Card>
<CardHeader>
<h2 className="text-lg font-semibold">🗄 Base de données</h2>
<p className="text-sm text-[var(--muted-foreground)]">
Informations et maintenance de la base de données
</p>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
<h3 className="font-medium mb-1">Tâches</h3>
<p className="text-2xl font-bold text-[var(--primary)]">
{dbStats.taskCount}
</p>
<p className="text-xs text-[var(--muted-foreground)]">entrées</p>
</div>
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
<h3 className="font-medium mb-1">Tags</h3>
<p className="text-2xl font-bold text-[var(--primary)]">
{dbStats.tagCount}
</p>
<p className="text-xs text-[var(--muted-foreground)]">entrées</p>
</div>
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
<h3 className="font-medium mb-1">Taux complétion</h3>
<p className="text-2xl font-bold text-[var(--primary)]">
{dbStats.completionRate}
</p>
<p className="text-xs text-[var(--muted-foreground)]">%</p>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
</div>
</UserPreferencesProvider>
);
}

View File

@@ -1,361 +0,0 @@
'use client';
import { useState, useMemo } from 'react';
import { UserPreferences, Tag } from '@/lib/types';
import { useTags } from '@/hooks/useTags';
import { Header } from '@/components/ui/Header';
import { Card, CardContent, CardHeader } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { TagForm } from '@/components/forms/TagForm';
import { UserPreferencesProvider } from '@/contexts/UserPreferencesContext';
import Link from 'next/link';
interface GeneralSettingsPageClientProps {
initialPreferences: UserPreferences;
initialTags: Tag[];
}
export function GeneralSettingsPageClient({ initialPreferences, initialTags }: GeneralSettingsPageClientProps) {
const {
tags,
refreshTags,
deleteTag
} = useTags(initialTags as (Tag & { usage: number })[]);
const [searchQuery, setSearchQuery] = useState('');
const [showOnlyUnused, setShowOnlyUnused] = useState(false);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [editingTag, setEditingTag] = useState<Tag | null>(null);
const [deletingTagId, setDeletingTagId] = useState<string | null>(null);
// Filtrer et trier les tags
const filteredTags = useMemo(() => {
let filtered = tags;
// Filtrer par recherche
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
filtered = filtered.filter(tag =>
tag.name.toLowerCase().includes(query)
);
}
// Filtrer pour afficher seulement les non utilisés
if (showOnlyUnused) {
filtered = filtered.filter(tag => {
const usage = (tag as Tag & { usage?: number }).usage || 0;
return usage === 0;
});
}
const sorted = filtered.sort((a, b) => {
const usageA = (a as Tag & { usage?: number }).usage || 0;
const usageB = (b as Tag & { usage?: number }).usage || 0;
if (usageB !== usageA) return usageB - usageA;
return a.name.localeCompare(b.name);
});
// Limiter à 12 tags si pas de recherche ni filtre, sinon afficher tous les résultats
const hasFilters = searchQuery.trim() || showOnlyUnused;
return hasFilters ? sorted : sorted.slice(0, 12);
}, [tags, searchQuery, showOnlyUnused]);
const handleEditTag = (tag: Tag) => {
setEditingTag(tag);
};
const handleDeleteTag = async (tag: Tag) => {
if (!confirm(`Êtes-vous sûr de vouloir supprimer le tag "${tag.name}" ?`)) {
return;
}
setDeletingTagId(tag.id);
try {
await deleteTag(tag.id);
await refreshTags();
} catch (error) {
console.error('Erreur lors de la suppression:', error);
} finally {
setDeletingTagId(null);
}
};
return (
<UserPreferencesProvider initialPreferences={initialPreferences}>
<div className="min-h-screen bg-[var(--background)]">
<Header
title="TowerControl"
subtitle="Paramètres généraux"
/>
<div className="container mx-auto px-4 py-4">
<div className="max-w-4xl mx-auto">
{/* Breadcrumb */}
<div className="mb-4 text-sm">
<Link href="/settings" className="text-[var(--muted-foreground)] hover:text-[var(--primary)]">
Paramètres
</Link>
<span className="mx-2 text-[var(--muted-foreground)]">/</span>
<span className="text-[var(--foreground)]">Général</span>
</div>
{/* Page Header */}
<div className="mb-6">
<h1 className="text-2xl font-mono font-bold text-[var(--foreground)] mb-2">
Paramètres généraux
</h1>
<p className="text-[var(--muted-foreground)]">
Configuration des préférences de l&apos;interface et du comportement général
</p>
</div>
<div className="space-y-6">
{/* Gestion des tags */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold flex items-center gap-2">
🏷 Gestion des tags
</h2>
<p className="text-sm text-[var(--muted-foreground)] mt-1">
Créer et organiser les étiquettes pour vos tâches
</p>
</div>
<Button
variant="primary"
size="sm"
onClick={() => setIsCreateModalOpen(true)}
className="flex items-center gap-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Nouveau tag
</Button>
</div>
</CardHeader>
<CardContent>
{/* Stats des tags */}
<div className="grid grid-cols-3 gap-4 mb-4">
<div className="text-center p-3 bg-[var(--muted)]/20 rounded">
<div className="text-xl font-bold text-[var(--foreground)]">{tags.length}</div>
<div className="text-sm text-[var(--muted-foreground)]">Tags créés</div>
</div>
<div className="text-center p-3 bg-[var(--primary)]/10 rounded">
<div className="text-xl font-bold text-[var(--primary)]">
{tags.reduce((sum, tag) => sum + ((tag as Tag & { usage?: number }).usage || 0), 0)}
</div>
<div className="text-sm text-[var(--muted-foreground)]">Utilisations</div>
</div>
<div className="text-center p-3 bg-[var(--success)]/10 rounded">
<div className="text-xl font-bold text-[var(--success)]">
{tags.filter(tag => (tag as Tag & { usage?: number }).usage && (tag as Tag & { usage?: number }).usage! > 0).length}
</div>
<div className="text-sm text-[var(--muted-foreground)]">Actifs</div>
</div>
</div>
{/* Recherche et filtres */}
<div className="space-y-3 mb-4">
<Input
placeholder="Rechercher un tag..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full"
/>
{/* Filtres rapides */}
<div className="flex items-center gap-3">
<Button
variant={showOnlyUnused ? "primary" : "ghost"}
size="sm"
onClick={() => setShowOnlyUnused(!showOnlyUnused)}
className="flex items-center gap-2"
>
<span className="text-xs"></span>
Tags non utilisés ({tags.filter(tag => ((tag as Tag & { usage?: number }).usage || 0) === 0).length})
</Button>
{(searchQuery || showOnlyUnused) && (
<Button
variant="ghost"
size="sm"
onClick={() => {
setSearchQuery('');
setShowOnlyUnused(false);
}}
className="text-[var(--muted-foreground)] hover:text-[var(--foreground)]"
>
Réinitialiser
</Button>
)}
</div>
</div>
{/* Liste des tags en grid */}
{filteredTags.length === 0 ? (
<div className="text-center py-8 text-[var(--muted-foreground)]">
{searchQuery && showOnlyUnused ? 'Aucun tag non utilisé trouvé avec cette recherche' :
searchQuery ? 'Aucun tag trouvé pour cette recherche' :
showOnlyUnused ? '🎉 Aucun tag non utilisé ! Tous vos tags sont actifs.' :
'Aucun tag créé'}
{!searchQuery && !showOnlyUnused && (
<div className="mt-2">
<Button
variant="ghost"
size="sm"
onClick={() => setIsCreateModalOpen(true)}
>
Créer votre premier tag
</Button>
</div>
)}
</div>
) : (
<div className="space-y-4">
{/* Grid des tags */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{filteredTags.map((tag) => {
const usage = (tag as Tag & { usage?: number }).usage || 0;
const isUnused = usage === 0;
return (
<div
key={tag.id}
className={`p-3 rounded-lg border transition-all hover:shadow-sm ${
isUnused
? 'border-[var(--destructive)]/30 bg-[var(--destructive)]/5 hover:border-[var(--destructive)]/50'
: 'border-[var(--border)] hover:border-[var(--primary)]/50'
}`}
>
{/* Header du tag */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2 flex-1 min-w-0">
<div
className="w-3 h-3 rounded-full flex-shrink-0"
style={{ backgroundColor: tag.color }}
/>
<span className="font-medium text-sm truncate">{tag.name}</span>
{tag.isPinned && (
<span className="text-xs px-1.5 py-0.5 bg-[var(--primary)]/20 text-[var(--primary)] rounded flex-shrink-0">
📌
</span>
)}
</div>
{/* Actions */}
<div className="flex items-center gap-1 flex-shrink-0">
<Button
variant="ghost"
size="sm"
onClick={() => handleEditTag(tag)}
className="h-7 w-7 p-0 text-[var(--muted-foreground)] hover:text-[var(--foreground)]"
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteTag(tag)}
disabled={deletingTagId === tag.id}
className={`h-7 w-7 p-0 ${
isUnused
? 'text-[var(--destructive)] hover:text-[var(--destructive)] hover:bg-[var(--destructive)]/20'
: 'text-[var(--muted-foreground)] hover:text-[var(--destructive)] hover:bg-[var(--destructive)]/10'
}`}
>
{deletingTagId === tag.id ? (
<svg className="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
) : (
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
)}
</Button>
</div>
</div>
{/* Stats et warning */}
<div className="space-y-1">
<div className={`text-xs flex items-center justify-between ${
isUnused ? 'text-[var(--destructive)]' : 'text-[var(--muted-foreground)]'
}`}>
<span>{usage} utilisation{usage !== 1 ? 's' : ''}</span>
{isUnused && (
<span className="text-xs px-1.5 py-0.5 bg-[var(--destructive)]/20 text-[var(--destructive)] rounded">
Non utilisé
</span>
)}
</div>
{('createdAt' in tag && (tag as Tag & { createdAt: Date }).createdAt) && (
<div className="text-xs text-[var(--muted-foreground)]">
Créé le {new Date((tag as Tag & { createdAt: Date }).createdAt).toLocaleDateString('fr-FR')}
</div>
)}
</div>
</div>
);
})}
</div>
{/* Message si plus de tags */}
{tags.length > 12 && !searchQuery && !showOnlyUnused && (
<div className="text-center pt-2 text-sm text-[var(--muted-foreground)]">
Et {tags.length - 12} autres tags... (utilisez la recherche ou les filtres pour les voir)
</div>
)}
</div>
)}
</CardContent>
</Card>
{/* Note développement futur */}
<Card>
<CardContent className="p-4">
<div className="p-4 bg-[var(--warning)]/10 border border-[var(--warning)]/20 rounded">
<p className="text-sm text-[var(--warning)] font-medium mb-2">
🚧 Interface de configuration en développement
</p>
<p className="text-xs text-[var(--muted-foreground)]">
Les contrôles interactifs pour modifier les autres préférences seront disponibles dans une prochaine version.
Pour l&apos;instant, les préférences sont modifiables via les boutons de l&apos;interface principale.
</p>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
</div>
{/* Modals pour les tags */}
{isCreateModalOpen && (
<TagForm
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
onSuccess={async () => {
setIsCreateModalOpen(false);
await refreshTags();
}}
/>
)}
{editingTag && (
<TagForm
isOpen={!!editingTag}
tag={editingTag}
onClose={() => setEditingTag(null)}
onSuccess={async () => {
setEditingTag(null);
await refreshTags();
}}
/>
)}
</UserPreferencesProvider>
);
}

View File

@@ -1,172 +0,0 @@
'use client';
import { UserPreferences, JiraConfig } from '@/lib/types';
import { Header } from '@/components/ui/Header';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { JiraConfigForm } from '@/components/settings/JiraConfigForm';
import { JiraSync } from '@/components/jira/JiraSync';
import { JiraLogs } from '@/components/jira/JiraLogs';
import { UserPreferencesProvider } from '@/contexts/UserPreferencesContext';
import Link from 'next/link';
interface IntegrationsSettingsPageClientProps {
initialPreferences: UserPreferences;
initialJiraConfig: JiraConfig;
}
export function IntegrationsSettingsPageClient({
initialPreferences,
initialJiraConfig
}: IntegrationsSettingsPageClientProps) {
return (
<UserPreferencesProvider initialPreferences={initialPreferences}>
<div className="min-h-screen bg-[var(--background)]">
<Header
title="TowerControl"
subtitle="Intégrations externes"
/>
<div className="container mx-auto px-4 py-4">
<div className="max-w-6xl mx-auto">
{/* Breadcrumb */}
<div className="mb-4 text-sm">
<Link href="/settings" className="text-[var(--muted-foreground)] hover:text-[var(--primary)]">
Paramètres
</Link>
<span className="mx-2 text-[var(--muted-foreground)]">/</span>
<span className="text-[var(--foreground)]">Intégrations</span>
</div>
{/* Page Header */}
<div className="mb-6">
<h1 className="text-2xl font-mono font-bold text-[var(--foreground)] mb-2">
🔌 Intégrations externes
</h1>
<p className="text-[var(--muted-foreground)]">
Configuration des intégrations avec les outils externes
</p>
</div>
{/* Layout en 2 colonnes pour optimiser l'espace */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
{/* Colonne principale: Configuration Jira */}
<div className="xl:col-span-2 space-y-6">
<Card>
<CardHeader>
<h2 className="text-xl font-semibold flex items-center gap-2">
<span className="text-blue-600">🏢</span>
Jira Cloud
</h2>
<p className="text-sm text-[var(--muted-foreground)]">
Synchronisation automatique des tickets Jira vers TowerControl
</p>
</CardHeader>
<CardContent>
<JiraConfigForm />
</CardContent>
</Card>
{/* Futures intégrations */}
<Card>
<CardHeader>
<h2 className="text-xl font-semibold">Autres intégrations</h2>
<p className="text-sm text-[var(--muted-foreground)]">
Intégrations prévues pour les prochaines versions
</p>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
<div className="flex items-center gap-2 mb-2">
<span className="text-lg">📧</span>
<h3 className="font-medium">Slack/Teams</h3>
</div>
<p className="text-sm text-[var(--muted-foreground)]">
Notifications et commandes via chat
</p>
</div>
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
<div className="flex items-center gap-2 mb-2">
<span className="text-lg">🐙</span>
<h3 className="font-medium">GitHub/GitLab</h3>
</div>
<p className="text-sm text-[var(--muted-foreground)]">
Synchronisation des issues et PR
</p>
</div>
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
<div className="flex items-center gap-2 mb-2">
<span className="text-lg">📊</span>
<h3 className="font-medium">Calendriers</h3>
</div>
<p className="text-sm text-[var(--muted-foreground)]">
Google Calendar, Outlook, etc.
</p>
</div>
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
<div className="flex items-center gap-2 mb-2">
<span className="text-lg"></span>
<h3 className="font-medium">Time tracking</h3>
</div>
<p className="text-sm text-[var(--muted-foreground)]">
Toggl, RescueTime, etc.
</p>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Colonne latérale: Actions et Logs Jira */}
<div className="space-y-4">
{initialJiraConfig?.enabled && (
<>
{/* Dashboard Analytics */}
{initialJiraConfig.projectKey && (
<Card>
<CardHeader>
<h3 className="text-sm font-semibold">📊 Analytics d&apos;équipe</h3>
</CardHeader>
<CardContent className="space-y-3">
<p className="text-xs text-[var(--muted-foreground)]">
Surveillance du projet {initialJiraConfig.projectKey}
</p>
<Link
href="/jira-dashboard"
className="inline-flex items-center justify-center w-full px-3 py-2 text-sm font-medium bg-[var(--primary)] text-[var(--primary-foreground)] rounded-lg hover:bg-[var(--primary)]/90 transition-colors"
>
Voir le Dashboard
</Link>
</CardContent>
</Card>
)}
<JiraSync />
<JiraLogs />
</>
)}
{!initialJiraConfig?.enabled && (
<Card>
<CardContent className="p-4">
<div className="text-center py-6">
<span className="text-4xl mb-4 block">🔧</span>
<p className="text-sm text-[var(--muted-foreground)]">
Configurez Jira pour accéder aux outils de synchronisation
</p>
</div>
</CardContent>
</Card>
)}
</div>
</div>
</div>
</div>
</div>
</UserPreferencesProvider>
);
}

View File

@@ -1,420 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { useJiraConfig } from '@/hooks/useJiraConfig';
import { jiraConfigClient } from '@/clients/jira-config-client';
export function JiraConfigForm() {
const { config, isLoading: configLoading, saveConfig, deleteConfig } = useJiraConfig();
const [formData, setFormData] = useState({
baseUrl: '',
email: '',
apiToken: '',
projectKey: '',
ignoredProjects: [] as string[]
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [isValidating, setIsValidating] = useState(false);
const [validationResult, setValidationResult] = useState<{ type: 'success' | 'error', text: string } | null>(null);
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
const [showForm, setShowForm] = useState(false);
// Charger les données existantes dans le formulaire
useEffect(() => {
if (config) {
setFormData({
baseUrl: config.baseUrl || '',
email: config.email || '',
apiToken: config.apiToken || '',
projectKey: config.projectKey || '',
ignoredProjects: config.ignoredProjects || []
});
}
}, [config]);
// Afficher le formulaire par défaut si Jira n'est pas configuré
useEffect(() => {
const isConfigured = config?.enabled && (config?.baseUrl || config?.email);
if (!configLoading && !isConfigured) {
setShowForm(true);
}
}, [config, configLoading]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
setMessage(null);
try {
const result = await saveConfig(formData);
if (result.success) {
setMessage({
type: 'success',
text: result.message
});
// Masquer le formulaire après une sauvegarde réussie
setShowForm(false);
} else {
setMessage({
type: 'error',
text: result.message
});
}
} catch (error) {
setMessage({
type: 'error',
text: error instanceof Error ? error.message : 'Erreur lors de la sauvegarde de la configuration'
});
} finally {
setIsSubmitting(false);
}
};
const handleDelete = async () => {
if (!confirm('Êtes-vous sûr de vouloir supprimer la configuration Jira ?')) {
return;
}
setIsSubmitting(true);
setMessage(null);
try {
const result = await deleteConfig();
if (result.success) {
setFormData({
baseUrl: '',
email: '',
apiToken: '',
projectKey: '',
ignoredProjects: []
});
setMessage({
type: 'success',
text: result.message
});
} else {
setMessage({
type: 'error',
text: result.message
});
}
} catch (error) {
setMessage({
type: 'error',
text: error instanceof Error ? error.message : 'Erreur lors de la suppression de la configuration'
});
} finally {
setIsSubmitting(false);
}
};
const handleValidateProject = async () => {
if (!formData.projectKey.trim()) {
setValidationResult({
type: 'error',
text: 'Veuillez saisir une clé de projet'
});
return;
}
setIsValidating(true);
setValidationResult(null);
try {
const result = await jiraConfigClient.validateProject(formData.projectKey);
if (result.success && result.exists) {
setValidationResult({
type: 'success',
text: `✓ Projet trouvé : ${result.projectName}`
});
} else {
setValidationResult({
type: 'error',
text: result.error || result.message
});
}
} catch (error) {
setValidationResult({
type: 'error',
text: error instanceof Error ? error.message : 'Erreur lors de la validation'
});
} finally {
setIsValidating(false);
}
};
const isJiraConfigured = config?.enabled && (config?.baseUrl || config?.email);
const isLoading = configLoading || isSubmitting;
return (
<div className="space-y-6">
{/* Statut actuel */}
<div className="flex items-center justify-between p-4 bg-[var(--card)] rounded border">
<div>
<h3 className="font-medium">Statut de l&apos;intégration</h3>
<p className="text-sm text-[var(--muted-foreground)]">
{isJiraConfigured
? 'Jira est configuré et prêt à être utilisé'
: 'Jira n\'est pas configuré'
}
</p>
</div>
<div className="flex items-center gap-3">
<Badge variant={isJiraConfigured ? 'success' : 'danger'}>
{isJiraConfigured ? '✓ Configuré' : '✗ Non configuré'}
</Badge>
<Button
variant="secondary"
size="sm"
onClick={() => setShowForm(!showForm)}
>
{showForm ? 'Masquer' : (isJiraConfigured ? 'Modifier' : 'Configurer')}
</Button>
</div>
</div>
{isJiraConfigured && (
<div className="p-4 bg-[var(--card)] rounded border">
<h3 className="font-medium mb-2">Configuration actuelle</h3>
<div className="space-y-2 text-sm">
<div>
<span className="text-[var(--muted-foreground)]">URL de base:</span>{' '}
<code className="bg-[var(--background)] px-2 py-1 rounded text-xs">
{config?.baseUrl || 'Non définie'}
</code>
</div>
<div>
<span className="text-[var(--muted-foreground)]">Email:</span>{' '}
<code className="bg-[var(--background)] px-2 py-1 rounded text-xs">
{config?.email || 'Non défini'}
</code>
</div>
<div>
<span className="text-[var(--muted-foreground)]">Token API:</span>{' '}
<code className="bg-[var(--background)] px-2 py-1 rounded text-xs">
{config?.apiToken ? '••••••••' : 'Non défini'}
</code>
</div>
<div>
<span className="text-[var(--muted-foreground)]">Projet surveillé:</span>{' '}
<code className="bg-[var(--background)] px-2 py-1 rounded text-xs">
{config?.projectKey || 'Non défini'}
</code>
</div>
<div>
<span className="text-[var(--muted-foreground)]">Projets ignorés:</span>{' '}
{config?.ignoredProjects && config.ignoredProjects.length > 0 ? (
<div className="mt-1 space-x-1">
{config.ignoredProjects.map(project => (
<code key={project} className="bg-[var(--background)] px-2 py-1 rounded text-xs">
{project}
</code>
))}
</div>
) : (
<span className="text-xs">Aucun</span>
)}
</div>
</div>
</div>
)}
{/* Formulaire de configuration */}
{showForm && (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">
URL de base Jira Cloud
</label>
<input
type="url"
value={formData.baseUrl}
onChange={(e) => setFormData(prev => ({ ...prev, baseUrl: e.target.value }))}
placeholder="https://votre-domaine.atlassian.net"
className="w-full px-3 py-2 border border-[var(--border)] rounded bg-[var(--background)] text-[var(--foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)] focus:border-transparent"
required
/>
<p className="text-xs text-[var(--muted-foreground)] mt-1">
L&apos;URL de votre instance Jira Cloud (ex: https://monentreprise.atlassian.net)
</p>
</div>
<div>
<label className="block text-sm font-medium mb-2">
Email Jira
</label>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
placeholder="votre-email@exemple.com"
className="w-full px-3 py-2 border border-[var(--border)] rounded bg-[var(--background)] text-[var(--foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)] focus:border-transparent"
required
/>
<p className="text-xs text-[var(--muted-foreground)] mt-1">
L&apos;email de votre compte Jira
</p>
</div>
<div>
<label className="block text-sm font-medium mb-2">
Token API Jira
</label>
<input
type="password"
value={formData.apiToken}
onChange={(e) => setFormData(prev => ({ ...prev, apiToken: e.target.value }))}
placeholder="Votre token API Jira"
className="w-full px-3 py-2 border border-[var(--border)] rounded bg-[var(--background)] text-[var(--foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)] focus:border-transparent"
required
/>
<p className="text-xs text-[var(--muted-foreground)] mt-1">
Créez un token API depuis{' '}
<a
href="https://id.atlassian.com/manage-profile/security/api-tokens"
target="_blank"
rel="noopener noreferrer"
className="text-[var(--primary)] hover:underline"
>
votre profil Atlassian
</a>
</p>
</div>
<div>
<label className="block text-sm font-medium mb-2">
Projet à surveiller (optionnel)
</label>
<div className="flex gap-2">
<input
type="text"
value={formData.projectKey}
onChange={(e) => {
setFormData(prev => ({ ...prev, projectKey: e.target.value.trim().toUpperCase() }));
setValidationResult(null); // Reset validation when input changes
}}
placeholder="MYTEAM"
className="flex-1 px-3 py-2 border border-[var(--border)] rounded bg-[var(--background)] text-[var(--foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)] focus:border-transparent"
/>
<Button
type="button"
variant="secondary"
onClick={handleValidateProject}
disabled={isValidating || !formData.projectKey.trim() || !isJiraConfigured}
className="px-4 shrink-0"
>
{isValidating ? 'Validation...' : 'Valider'}
</Button>
</div>
{/* Résultat de la validation */}
{validationResult && (
<div className={`mt-2 p-2 rounded text-sm ${
validationResult.type === 'success'
? 'bg-green-50 border border-green-200 text-green-800 dark:bg-green-900/20 dark:border-green-800 dark:text-green-200'
: 'bg-red-50 border border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-200'
}`}>
{validationResult.text}
</div>
)}
<p className="text-xs text-[var(--muted-foreground)] mt-1">
Clé du projet pour les analytics d&apos;équipe (ex: MYTEAM, DEV, PROD).
Laissez vide pour désactiver la surveillance d&apos;équipe.
</p>
</div>
<div>
<label className="block text-sm font-medium mb-2">
Projets à ignorer (optionnel)
</label>
<input
type="text"
value={formData.ignoredProjects.join(', ')}
onChange={(e) => {
const projects = e.target.value
.split(',')
.map(p => p.trim().toUpperCase())
.filter(p => p.length > 0);
setFormData(prev => ({ ...prev, ignoredProjects: projects }));
}}
placeholder="DEMO, TEST, SANDBOX"
className="w-full px-3 py-2 border border-[var(--border)] rounded bg-[var(--background)] text-[var(--foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)] focus:border-transparent"
/>
<p className="text-xs text-[var(--muted-foreground)] mt-1">
Liste des clés de projets à ignorer lors de la synchronisation, séparées par des virgules (ex: DEMO, TEST, SANDBOX).
Ces projets ne seront pas synchronisés vers TowerControl.
</p>
{formData.ignoredProjects.length > 0 && (
<div className="mt-2 space-x-1">
<span className="text-xs text-[var(--muted-foreground)]">Projets qui seront ignorés:</span>
{formData.ignoredProjects.map(project => (
<code key={project} className="bg-[var(--muted)] text-[var(--muted-foreground)] px-2 py-1 rounded text-xs">
{project}
</code>
))}
</div>
)}
</div>
<div className="flex gap-3">
<Button
type="submit"
disabled={isLoading}
className="flex-1"
>
{isLoading ? 'Sauvegarde...' : 'Sauvegarder la configuration'}
</Button>
{isJiraConfigured && (
<Button
type="button"
variant="secondary"
onClick={handleDelete}
disabled={isLoading}
className="px-6"
>
Supprimer
</Button>
)}
</div>
{/* Instructions */}
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
<h3 className="font-medium mb-2">💡 Instructions de configuration</h3>
<div className="text-sm text-[var(--muted-foreground)] space-y-2">
<p><strong>1. URL de base:</strong> Votre domaine Jira Cloud (ex: https://monentreprise.atlassian.net)</p>
<p><strong>2. Email:</strong> L&apos;email de votre compte Jira/Atlassian</p>
<p><strong>3. Token API:</strong> Créez un token depuis votre profil Atlassian :</p>
<ul className="ml-4 space-y-1 list-disc">
<li>Allez sur <a href="https://id.atlassian.com/manage-profile/security/api-tokens" target="_blank" rel="noopener noreferrer" className="text-[var(--primary)] hover:underline">id.atlassian.com</a></li>
<li>Cliquez sur &quot;Create API token&quot;</li>
<li>Donnez un nom descriptif (ex: &quot;TowerControl&quot;)</li>
<li>Copiez le token généré</li>
</ul>
<p className="mt-3 text-xs">
<strong>Note:</strong> Ces variables doivent être configurées dans l&apos;environnement du serveur (JIRA_BASE_URL, JIRA_EMAIL, JIRA_API_TOKEN)
</p>
</div>
</div>
</form>
)}
{message && (
<div className={`p-4 rounded border ${
message.type === 'success'
? 'bg-green-50 border-green-200 text-green-800 dark:bg-green-900/20 dark:border-green-800 dark:text-green-200'
: 'bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-200'
}`}>
{message.text}
</div>
)}
</div>
);
}

View File

@@ -1,220 +0,0 @@
'use client';
import { UserPreferences } from '@/lib/types';
import { Header } from '@/components/ui/Header';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { UserPreferencesProvider } from '@/contexts/UserPreferencesContext';
import Link from 'next/link';
interface SettingsIndexPageClientProps {
initialPreferences: UserPreferences;
}
export function SettingsIndexPageClient({ initialPreferences }: SettingsIndexPageClientProps) {
const settingsPages = [
{
href: '/settings/general',
icon: '⚙️',
title: 'Paramètres généraux',
description: 'Interface, thème, préférences d\'affichage',
status: 'En développement'
},
{
href: '/settings/integrations',
icon: '🔌',
title: 'Intégrations',
description: 'Jira, GitHub, Slack et autres services externes',
status: 'Fonctionnel'
},
{
href: '/settings/advanced',
icon: '🛠️',
title: 'Paramètres avancés',
description: 'Sauvegarde, logs, debug et maintenance',
status: 'Prochainement'
}
];
return (
<UserPreferencesProvider initialPreferences={initialPreferences}>
<div className="min-h-screen bg-[var(--background)]">
<Header
title="TowerControl"
subtitle="Configuration & Paramètres"
/>
<div className="container mx-auto px-4 py-4">
<div className="max-w-4xl mx-auto">
{/* Page Header */}
<div className="mb-8">
<h1 className="text-3xl font-mono font-bold text-[var(--foreground)] mb-3">
Paramètres
</h1>
<p className="text-[var(--muted-foreground)] text-lg">
Configuration de TowerControl et de ses intégrations
</p>
</div>
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<span className="text-2xl">🎨</span>
<div>
<p className="text-sm text-[var(--muted-foreground)]">Thème actuel</p>
<p className="font-medium capitalize">{initialPreferences.viewPreferences.theme}</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<span className="text-2xl">🔌</span>
<div>
<p className="text-sm text-[var(--muted-foreground)]">Jira</p>
<p className="font-medium">
{initialPreferences.jiraConfig.enabled ? 'Configuré' : 'Non configuré'}
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<span className="text-2xl">📏</span>
<div>
<p className="text-sm text-[var(--muted-foreground)]">Taille police</p>
<p className="font-medium capitalize">{initialPreferences.viewPreferences.fontSize}</p>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Settings Sections */}
<div className="space-y-4">
<h2 className="text-xl font-semibold text-[var(--foreground)] mb-4">
Sections de configuration
</h2>
<div className="grid grid-cols-1 md:grid-cols-1 gap-4">
{settingsPages.map((page) => (
<Link key={page.href} href={page.href}>
<Card className="transition-all hover:shadow-md hover:border-[var(--primary)]/30 cursor-pointer">
<CardContent className="p-6">
<div className="flex items-start justify-between">
<div className="flex items-start gap-4">
<span className="text-3xl">{page.icon}</span>
<div className="flex-1">
<h3 className="text-lg font-semibold text-[var(--foreground)] mb-1">
{page.title}
</h3>
<p className="text-[var(--muted-foreground)] mb-2">
{page.description}
</p>
<div className="flex items-center gap-2">
<span className={`px-2 py-1 rounded text-xs font-medium ${
page.status === 'Fonctionnel'
? 'bg-[var(--success)]/20 text-[var(--success)]'
: page.status === 'En développement'
? 'bg-[var(--warning)]/20 text-[var(--warning)]'
: 'bg-[var(--muted)]/20 text-[var(--muted-foreground)]'
}`}>
{page.status}
</span>
</div>
</div>
</div>
<svg
className="w-5 h-5 text-[var(--muted-foreground)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</CardContent>
</Card>
</Link>
))}
</div>
</div>
{/* Quick Actions */}
<div className="mt-8">
<h2 className="text-xl font-semibold text-[var(--foreground)] mb-4">
Actions rapides
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<h3 className="font-medium mb-1">Sauvegarde manuelle</h3>
<p className="text-sm text-[var(--muted-foreground)]">
Créer une sauvegarde des données
</p>
</div>
<button className="px-3 py-1.5 bg-[var(--primary)] text-[var(--primary-foreground)] rounded text-sm">
Sauvegarder
</button>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<h3 className="font-medium mb-1">Test Jira</h3>
<p className="text-sm text-[var(--muted-foreground)]">
Tester la connexion Jira
</p>
</div>
<button
className="px-3 py-1.5 bg-[var(--card)] text-[var(--foreground)] border border-[var(--border)] rounded text-sm"
disabled={!initialPreferences.jiraConfig.enabled}
>
Tester
</button>
</div>
</CardContent>
</Card>
</div>
</div>
{/* System Info */}
<Card className="mt-8">
<CardHeader>
<h2 className="text-lg font-semibold"> Informations système</h2>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div>
<p className="text-[var(--muted-foreground)]">Version</p>
<p className="font-medium">TowerControl v1.0.0</p>
</div>
<div>
<p className="text-[var(--muted-foreground)]">Dernière maj</p>
<p className="font-medium">Il y a 2 jours</p>
</div>
<div>
<p className="text-[var(--muted-foreground)]">Env</p>
<p className="font-medium">Development</p>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
</UserPreferencesProvider>
);
}

View File

@@ -1,143 +0,0 @@
'use client';
import { useState } from 'react';
import { Header } from '@/components/ui/Header';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { JiraConfigForm } from '@/components/settings/JiraConfigForm';
import { JiraSync } from '@/components/jira/JiraSync';
import { JiraLogs } from '@/components/jira/JiraLogs';
import { useJiraConfig } from '@/hooks/useJiraConfig';
export function SettingsPageClient() {
const { config: jiraConfig } = useJiraConfig();
const [activeTab, setActiveTab] = useState<'general' | 'integrations' | 'advanced'>('general');
const tabs = [
{ id: 'general' as const, label: 'Général', icon: '⚙️' },
{ id: 'integrations' as const, label: 'Intégrations', icon: '🔌' },
{ id: 'advanced' as const, label: 'Avancé', icon: '🛠️' }
];
return (
<div className="min-h-screen bg-[var(--background)]">
<Header
title="TowerControl"
subtitle="Configuration & Paramètres"
/>
<div className="container mx-auto px-4 py-4">
<div className="max-w-7xl mx-auto">
{/* En-tête compact */}
<div className="mb-4">
<h1 className="text-xl font-mono font-bold text-[var(--foreground)] mb-1">
Paramètres
</h1>
<p className="text-sm text-[var(--muted-foreground)]">
Configuration de TowerControl et de ses intégrations
</p>
</div>
<div className="flex gap-6">
{/* Navigation latérale compacte */}
<div className="w-56 flex-shrink-0">
<Card>
<CardContent className="p-0">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`w-full flex items-center gap-2 px-3 py-2 text-left transition-colors ${
activeTab === tab.id
? 'bg-[var(--primary)]/10 text-[var(--primary)] border-r-2 border-[var(--primary)]'
: 'text-[var(--muted-foreground)] hover:bg-[var(--card-hover)] hover:text-[var(--foreground)]'
}`}
>
<span className="text-base">{tab.icon}</span>
<span className="font-medium text-sm">{tab.label}</span>
</button>
))}
</CardContent>
</Card>
</div>
{/* Contenu principal */}
<div className="flex-1 min-h-0">
{activeTab === 'general' && (
<div className="space-y-6">
<Card>
<CardHeader>
<h2 className="text-lg font-semibold">Préférences générales</h2>
</CardHeader>
<CardContent className="space-y-4">
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
<p className="text-sm text-[var(--muted-foreground)]">
Les paramètres généraux seront disponibles dans une prochaine version.
</p>
</div>
</CardContent>
</Card>
</div>
)}
{activeTab === 'integrations' && (
<div className="h-full">
{/* Layout en 2 colonnes pour optimiser l'espace */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-4 h-full">
{/* Colonne 1: Configuration Jira */}
<div className="xl:col-span-2">
<Card className="h-fit">
<CardHeader className="pb-3">
<h2 className="text-base font-semibold">🔌 Intégration Jira Cloud</h2>
<p className="text-xs text-[var(--muted-foreground)]">
Synchronisation automatique des tickets
</p>
</CardHeader>
<CardContent>
<JiraConfigForm />
</CardContent>
</Card>
</div>
{/* Colonne 2: Actions et Logs */}
<div className="space-y-4">
{jiraConfig?.enabled && (
<>
<JiraSync />
<JiraLogs />
</>
)}
</div>
</div>
</div>
)}
{activeTab === 'advanced' && (
<div className="space-y-6">
<Card>
<CardHeader>
<h2 className="text-lg font-semibold">Paramètres avancés</h2>
</CardHeader>
<CardContent className="space-y-4">
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
<p className="text-sm text-[var(--muted-foreground)]">
Les paramètres avancés seront disponibles dans une prochaine version.
</p>
<ul className="mt-2 text-xs text-[var(--muted-foreground)] space-y-1">
<li> Configuration de la base de données</li>
<li> Logs de debug</li>
<li> Export/Import des données</li>
<li> Réinitialisation</li>
</ul>
</div>
</CardContent>
</Card>
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,44 +0,0 @@
import { HTMLAttributes, forwardRef } from 'react';
import { cn } from '@/lib/utils';
interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'outline';
size?: 'sm' | 'md';
}
const Badge = forwardRef<HTMLSpanElement, BadgeProps>(
({ className, variant = 'default', size = 'md', ...props }, ref) => {
const baseStyles = 'inline-flex items-center font-mono font-medium transition-all duration-200';
const variants = {
default: 'bg-[var(--card)] text-[var(--muted-foreground)] border border-[var(--border)]',
primary: 'bg-[var(--primary)]/20 text-[var(--primary)] border border-[var(--primary)]/30',
success: 'bg-[var(--success)]/20 text-[var(--success)] border border-[var(--success)]/30',
warning: 'bg-[var(--accent)]/20 text-[var(--accent)] border border-[var(--accent)]/30',
danger: 'bg-[var(--destructive)]/20 text-[var(--destructive)] border border-[var(--destructive)]/30',
outline: 'bg-transparent text-[var(--muted-foreground)] border border-[var(--border)] hover:bg-[var(--card-hover)] hover:text-[var(--foreground)]'
};
const sizes = {
sm: 'px-1.5 py-0.5 text-xs rounded',
md: 'px-2 py-1 text-xs rounded-md'
};
return (
<span
className={cn(
baseStyles,
variants[variant],
sizes[size],
className
)}
ref={ref}
{...props}
/>
);
}
);
Badge.displayName = 'Badge';
export { Badge };

View File

@@ -1,43 +0,0 @@
import { ButtonHTMLAttributes, forwardRef } from 'react';
import { cn } from '@/lib/utils';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
size?: 'sm' | 'md' | 'lg';
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = 'primary', size = 'md', ...props }, ref) => {
const baseStyles = 'inline-flex items-center justify-center font-mono font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-[var(--background)] disabled:opacity-50 disabled:cursor-not-allowed';
const variants = {
primary: 'bg-[var(--primary)] hover:bg-[var(--primary)]/80 text-[var(--primary-foreground)] border border-[var(--primary)]/30 shadow-[var(--primary)]/20 shadow-lg hover:shadow-[var(--primary)]/30 focus:ring-[var(--primary)]',
secondary: 'bg-[var(--card)] hover:bg-[var(--card-hover)] text-[var(--foreground)] border border-[var(--border)] shadow-[var(--muted)]/20 shadow-lg hover:shadow-[var(--muted)]/30 focus:ring-[var(--muted)]',
danger: 'bg-[var(--destructive)] hover:bg-[var(--destructive)]/80 text-white border border-[var(--destructive)]/30 shadow-[var(--destructive)]/20 shadow-lg hover:shadow-[var(--destructive)]/30 focus:ring-[var(--destructive)]',
ghost: 'bg-transparent hover:bg-[var(--card)]/50 text-[var(--muted-foreground)] hover:text-[var(--foreground)] border border-[var(--border)]/50 hover:border-[var(--border)] focus:ring-[var(--muted)]'
};
const sizes = {
sm: 'px-3 py-1.5 text-xs rounded-md',
md: 'px-4 py-2 text-sm rounded-lg',
lg: 'px-6 py-3 text-base rounded-lg'
};
return (
<button
className={cn(
baseStyles,
variants[variant],
sizes[size],
className
)}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = 'Button';
export { Button };

View File

@@ -1,81 +0,0 @@
import { HTMLAttributes, forwardRef } from 'react';
import { cn } from '@/lib/utils';
interface CardProps extends HTMLAttributes<HTMLDivElement> {
variant?: 'default' | 'elevated' | 'bordered' | 'column';
}
const Card = forwardRef<HTMLDivElement, CardProps>(
({ className, variant = 'default', ...props }, ref) => {
const variants = {
default: 'bg-[var(--card)]/50 border border-[var(--border)]/50',
elevated: 'bg-[var(--card)]/80 border border-[var(--border)]/50 shadow-lg shadow-[var(--card)]/20',
bordered: 'bg-[var(--card)]/50 border border-[var(--primary)]/30 shadow-[var(--primary)]/10 shadow-lg',
column: 'bg-[var(--card-column)] border border-[var(--border)]/50 shadow-lg shadow-[var(--card)]/20'
};
return (
<div
ref={ref}
className={cn(
'rounded-lg backdrop-blur-sm transition-all duration-200',
variants[variant],
className
)}
{...props}
/>
);
}
);
Card.displayName = 'Card';
const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('p-4 border-b border-[var(--border)]/50', className)}
{...props}
/>
)
);
CardHeader.displayName = 'CardHeader';
const CardTitle = forwardRef<HTMLHeadingElement, HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn('font-mono font-semibold text-[var(--foreground)] tracking-wide', className)}
{...props}
/>
)
);
CardTitle.displayName = 'CardTitle';
const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('p-4', className)}
{...props}
/>
)
);
CardContent.displayName = 'CardContent';
const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('p-4 border-t border-[var(--border)]/50', className)}
{...props}
/>
)
);
CardFooter.displayName = 'CardFooter';
export { Card, CardHeader, CardTitle, CardContent, CardFooter };

View File

@@ -1,208 +0,0 @@
'use client';
import { useTheme } from '@/contexts/ThemeContext';
import { useJiraConfig } from '@/contexts/JiraConfigContext';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
import { useState } from 'react';
interface HeaderProps {
title?: string;
subtitle?: string;
syncing?: boolean;
}
export function Header({ title = "TowerControl", subtitle = "Task Management", syncing = false }: HeaderProps) {
const { theme, toggleTheme } = useTheme();
const { isConfigured: isJiraConfigured, config: jiraConfig } = useJiraConfig();
const pathname = usePathname();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
// Fonction pour déterminer si un lien est actif
const isActiveLink = (href: string) => {
if (href === '/') {
return pathname === '/';
}
return pathname.startsWith(href);
};
// Fonction pour obtenir les classes CSS d'un lien (desktop)
const getLinkClasses = (href: string) => {
const baseClasses = "font-mono text-sm uppercase tracking-wider transition-colors px-3 py-1.5 rounded-md";
if (isActiveLink(href)) {
return `${baseClasses} text-[var(--primary)] bg-[var(--primary)]/10 border border-[var(--primary)]/30`;
}
return `${baseClasses} text-[var(--muted-foreground)] hover:text-[var(--primary)] hover:bg-[var(--card-hover)]`;
};
// Fonction pour obtenir les classes CSS d'un lien (mobile)
const getMobileLinkClasses = (href: string) => {
const baseClasses = "font-mono text-sm uppercase tracking-wider transition-colors px-4 py-3 rounded-md block w-full text-left";
if (isActiveLink(href)) {
return `${baseClasses} text-[var(--primary)] bg-[var(--primary)]/10 border border-[var(--primary)]/30`;
}
return `${baseClasses} text-[var(--muted-foreground)] hover:text-[var(--primary)] hover:bg-[var(--card-hover)]`;
};
// Liste des liens de navigation
const navLinks = [
{ href: '/', label: 'Dashboard' },
{ href: '/kanban', label: 'Kanban' },
{ href: '/daily', label: 'Daily' },
{ href: '/weekly-manager', label: 'Manager' },
...(isJiraConfigured ? [{ href: '/jira-dashboard', label: `Jira${jiraConfig?.projectKey ? ` (${jiraConfig.projectKey})` : ''}` }] : []),
{ href: '/settings', label: 'Settings' }
];
return (
<header className="relative z-50 bg-[var(--card)]/80 backdrop-blur-sm border-b border-[var(--border)]/50 shadow-lg shadow-[var(--card)]/20">
<div className="container mx-auto px-4 sm:px-6 py-4">
{/* Layout mobile/tablette */}
<div className="lg:hidden">
<div className="flex items-center justify-between">
{/* Titre et status */}
<div className="flex items-center gap-3 sm:gap-4 min-w-0 flex-1">
<div className={`w-3 h-3 rounded-full shadow-lg flex-shrink-0 ${
syncing
? 'bg-yellow-400 animate-spin shadow-yellow-400/50'
: 'bg-cyan-400 animate-pulse shadow-cyan-400/50'
}`}></div>
<div className="min-w-0">
<h1 className="text-xl sm:text-2xl font-mono font-bold text-[var(--foreground)] tracking-wider truncate">
{title}
</h1>
<p className="text-[var(--muted-foreground)] mt-1 font-mono text-xs sm:text-sm truncate">
{subtitle}
</p>
</div>
</div>
{/* Controls mobile/tablette */}
<div className="flex items-center gap-2 flex-shrink-0">
{/* Theme Toggle */}
<button
onClick={toggleTheme}
className="text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors p-2 rounded-md hover:bg-[var(--card-hover)]"
title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`}
>
{theme === 'dark' ? (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
) : (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
)}
</button>
{/* Menu burger */}
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors p-2 rounded-md hover:bg-[var(--card-hover)]"
title="Toggle menu"
>
{mobileMenuOpen ? (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
) : (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
)}
</button>
</div>
</div>
</div>
{/* Layout desktop - une seule ligne comme avant */}
<div className="hidden lg:flex items-center justify-between gap-6">
{/* Titre et status */}
<div className="flex items-center gap-6">
<div className="flex items-center gap-4 w-[300px]">
<div className={`w-3 h-3 rounded-full shadow-lg ${
syncing
? 'bg-yellow-400 animate-spin shadow-yellow-400/50'
: 'bg-cyan-400 animate-pulse shadow-cyan-400/50'
}`}></div>
<div>
<h1 className="text-2xl font-mono font-bold text-[var(--foreground)] tracking-wider">
{title}
</h1>
<p className="text-[var(--muted-foreground)] mt-1 font-mono text-sm">
{subtitle}
</p>
</div>
</div>
{/* Navigation desktop */}
<nav className="flex items-center gap-2">
{navLinks.map(({ href, label }) => (
<Link
key={href}
href={href}
className={getLinkClasses(href)}
>
{label}
</Link>
))}
{/* Theme Toggle desktop */}
<button
onClick={toggleTheme}
className="text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors p-1 rounded-md hover:bg-[var(--card-hover)]"
title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`}
>
{theme === 'dark' ? (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
) : (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
)}
</button>
</nav>
</div>
</div>
</div>
{/* Menu mobile/tablette en overlay fixe */}
{mobileMenuOpen && (
<>
{/* Backdrop pour fermer le menu */}
<div
className="lg:hidden fixed inset-0 bg-black/20 backdrop-blur-sm z-[100]"
onClick={() => setMobileMenuOpen(false)}
/>
{/* Menu */}
<div className="lg:hidden fixed top-[80px] left-0 right-0 bg-[var(--card)]/98 backdrop-blur-md border-b border-[var(--border)]/50 shadow-xl z-[101]">
<nav className="container mx-auto px-4 py-6">
<div className="space-y-3">
{navLinks.map(({ href, label }) => (
<Link
key={href}
href={href}
className={getMobileLinkClasses(href)}
onClick={() => setMobileMenuOpen(false)}
>
{label}
</Link>
))}
</div>
</nav>
</div>
</>
)}
</header>
);
}

View File

@@ -1,21 +0,0 @@
'use client';
import { Header } from './Header';
import { useTasks } from '@/hooks/useTasks';
interface HeaderContainerProps {
title: string;
subtitle: string;
}
export function HeaderContainer({ title, subtitle }: HeaderContainerProps) {
const { syncing } = useTasks();
return (
<Header
title={title}
subtitle={subtitle}
syncing={syncing}
/>
);
}

View File

@@ -1,44 +0,0 @@
import { InputHTMLAttributes, forwardRef } from 'react';
import { cn } from '@/lib/utils';
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ className, label, error, ...props }, ref) => {
return (
<div className="space-y-2">
{label && (
<label className="block text-sm font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
{label}
</label>
)}
<input
className={cn(
'w-full px-3 py-2 bg-[var(--input)] border border-[var(--border)]/50 rounded-lg',
'text-[var(--foreground)] font-mono text-sm placeholder-[var(--muted-foreground)]',
'focus:outline-none focus:ring-2 focus:ring-[var(--primary)]/50 focus:border-[var(--primary)]/50',
'hover:border-[var(--border)] transition-all duration-200',
'backdrop-blur-sm',
error && 'border-[var(--destructive)]/50 focus:ring-[var(--destructive)]/50 focus:border-[var(--destructive)]/50',
className
)}
ref={ref}
{...props}
/>
{error && (
<p className="text-xs font-mono text-[var(--destructive)] flex items-center gap-1">
<span className="text-[var(--destructive)]"></span>
{error}
</p>
)}
</div>
);
}
);
Input.displayName = 'Input';
export { Input };

View File

@@ -1,199 +0,0 @@
'use client';
import { useState, useRef, useEffect } from 'react';
import { Tag } from '@/lib/types';
import { useTagsAutocomplete } from '@/hooks/useTags';
import { Badge } from './Badge';
interface TagInputProps {
tags: string[];
onChange: (tags: string[]) => void;
placeholder?: string;
maxTags?: number;
className?: string;
compactSuggestions?: boolean; // Pour adapter selon l'espace
}
export function TagInput({
tags,
onChange,
placeholder = "Ajouter des tags...",
maxTags = 10,
className = "",
compactSuggestions = false
}: TagInputProps) {
const [inputValue, setInputValue] = useState('');
const [showSuggestions, setShowSuggestions] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(-1);
const inputRef = useRef<HTMLInputElement>(null);
const suggestionsRef = useRef<HTMLDivElement>(null);
const { suggestions, loading, searchTags, clearSuggestions, loadPopularTags } = useTagsAutocomplete();
// Rechercher des suggestions quand l'input change
useEffect(() => {
if (inputValue.trim()) {
searchTags(inputValue);
setShowSuggestions(true);
setSelectedIndex(-1);
} else {
clearSuggestions();
setShowSuggestions(false);
}
}, [inputValue, searchTags, clearSuggestions]);
const addTag = (tagName: string) => {
const trimmedTag = tagName.trim();
if (trimmedTag && !tags.includes(trimmedTag) && tags.length < maxTags) {
onChange([...tags, trimmedTag]);
}
setInputValue('');
setShowSuggestions(false);
setSelectedIndex(-1);
};
const removeTag = (tagToRemove: string) => {
onChange(tags.filter(tag => tag !== tagToRemove));
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
if (selectedIndex >= 0 && suggestions[selectedIndex]) {
addTag(suggestions[selectedIndex].name);
} else if (inputValue.trim()) {
addTag(inputValue);
}
} else if (e.key === 'Escape') {
setShowSuggestions(false);
setSelectedIndex(-1);
} else if (e.key === 'ArrowDown') {
e.preventDefault();
setSelectedIndex(prev =>
prev < suggestions.length - 1 ? prev + 1 : prev
);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectedIndex(prev => prev > 0 ? prev - 1 : -1);
} else if (e.key === 'Backspace' && !inputValue && tags.length > 0) {
// Supprimer le dernier tag si l'input est vide
removeTag(tags[tags.length - 1]);
}
};
const handleSuggestionClick = (tag: Tag) => {
addTag(tag.name);
};
const handleBlur = (e: React.FocusEvent) => {
// Délai pour permettre le clic sur une suggestion
setTimeout(() => {
if (!suggestionsRef.current?.contains(e.relatedTarget as Node)) {
setShowSuggestions(false);
setSelectedIndex(-1);
}
}, 150);
};
const handleFocus = () => {
if (inputValue.trim()) {
// Si il y a du texte, afficher les suggestions existantes
setShowSuggestions(true);
} else {
// Si l'input est vide, charger les tags populaires
loadPopularTags(20);
setShowSuggestions(true);
}
setSelectedIndex(-1);
};
return (
<div className={`relative ${className}`}>
{/* Container des tags et input */}
<div className="min-h-[42px] p-2 border border-[var(--border)] rounded-lg bg-[var(--input)] focus-within:border-[var(--primary)] focus-within:ring-1 focus-within:ring-[var(--primary)]/20 transition-colors">
<div className="flex flex-wrap gap-1 items-center">
{/* Tags existants */}
{tags.map((tag, index) => (
<Badge
key={index}
variant="default"
className="flex items-center gap-1 px-2 py-1 text-xs"
>
<span>{tag}</span>
<button
type="button"
onClick={() => removeTag(tag)}
className="text-[var(--muted-foreground)] hover:text-[var(--foreground)] ml-1"
aria-label={`Supprimer le tag ${tag}`}
>
×
</button>
</Badge>
))}
{/* Input pour nouveau tag */}
{tags.length < maxTags && (
<input
ref={inputRef}
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
onFocus={handleFocus}
placeholder={tags.length === 0 ? placeholder : ""}
className="flex-1 min-w-[120px] bg-transparent border-none outline-none text-[var(--foreground)] placeholder-[var(--muted-foreground)] text-sm"
/>
)}
</div>
</div>
{/* Suggestions dropdown */}
{showSuggestions && (suggestions.length > 0 || loading) && (
<div
ref={suggestionsRef}
className="absolute top-full left-0 right-0 mt-1 bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-lg z-[9999] max-h-64 overflow-y-auto"
>
{loading ? (
<div className="p-3 text-center text-[var(--muted-foreground)] text-sm">
Recherche...
</div>
) : (
<div className={`gap-2 p-3 ${compactSuggestions ? 'grid grid-cols-1' : 'grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4'}`}>
{suggestions.map((tag, index) => (
<button
key={tag.id}
type="button"
onClick={() => handleSuggestionClick(tag)}
className={`flex items-center gap-2 px-2 py-1.5 text-xs rounded-md transition-colors ${
index === selectedIndex
? 'bg-[var(--card-hover)] text-[var(--primary)] ring-1 ring-[var(--primary)]'
: 'text-[var(--foreground)] hover:bg-[var(--card-hover)]'
} ${tags.includes(tag.name) ? 'opacity-50 cursor-not-allowed' : ''}`}
disabled={tags.includes(tag.name)}
title={tags.includes(tag.name) ? 'Déjà ajouté' : `Ajouter ${tag.name}`}
>
<div
className="w-2 h-2 rounded-full flex-shrink-0"
style={{ backgroundColor: tag.color }}
/>
<span className="truncate">{tag.name}</span>
{tags.includes(tag.name) && (
<span className="text-[var(--muted-foreground)] ml-auto"></span>
)}
</button>
))}
</div>
)}
</div>
)}
{/* Indicateur de limite */}
{tags.length >= maxTags && (
<div className="text-xs text-[var(--muted-foreground)] mt-1">
Limite de {maxTags} tags atteinte
</div>
)}
</div>
);
}

View File

@@ -1,95 +0,0 @@
import { Tag } from '@/lib/types';
interface TagListProps {
tags: (Tag & { usage?: number })[];
onTagEdit?: (tag: Tag) => void;
onTagDelete?: (tag: Tag) => void;
showActions?: boolean;
showUsage?: boolean;
deletingTagId?: string | null;
}
export function TagList({
tags,
onTagEdit,
onTagDelete,
showActions = true,
deletingTagId
}: TagListProps) {
if (tags.length === 0) {
return (
<div className="text-center py-12 text-slate-400">
<div className="text-6xl mb-4">🏷</div>
<p className="text-lg mb-2">Aucun tag trouvé</p>
<p className="text-sm">Créez votre premier tag pour commencer</p>
</div>
);
}
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{tags.map((tag) => {
const isDeleting = deletingTagId === tag.id;
return (
<div
key={tag.id}
className={`group relative bg-slate-800/50 rounded-lg border border-slate-700 hover:border-slate-600 transition-all duration-200 hover:shadow-lg hover:shadow-slate-900/20 p-3 ${
isDeleting ? 'opacity-50 pointer-events-none' : ''
}`}
>
{/* Contenu principal */}
<div className="flex items-center gap-3">
<div
className="w-5 h-5 rounded-full shadow-sm"
style={{ backgroundColor: tag.color }}
/>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<h3 className="text-slate-200 font-medium truncate">
{tag.name}
</h3>
{tag.usage !== undefined && (
<span className="text-xs text-slate-400 bg-slate-700/50 px-2 py-1 rounded-full ml-2 flex-shrink-0">
{tag.usage}
</span>
)}
</div>
</div>
</div>
{/* Actions (apparaissent au hover) */}
{showActions && (onTagEdit || onTagDelete) && (
<div className="absolute top-2 right-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
{onTagEdit && (
<button
onClick={() => onTagEdit(tag)}
className="h-7 px-2 text-xs bg-slate-800/50 backdrop-blur-sm border border-slate-700 hover:border-slate-600 hover:bg-slate-700/50 rounded-md transition-all duration-200 text-slate-300 hover:text-slate-200"
>
</button>
)}
{onTagDelete && (
<button
onClick={() => onTagDelete(tag)}
disabled={isDeleting}
className="h-7 px-2 text-xs bg-slate-800/50 backdrop-blur-sm border border-slate-700 hover:border-red-500/50 hover:text-red-400 hover:bg-red-900/20 rounded-md transition-all duration-200 text-slate-300 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isDeleting ? '⏳' : '🗑️'}
</button>
)}
</div>
)}
{/* Indicateur de couleur en bas */}
<div
className="absolute bottom-0 left-0 right-0 h-1 rounded-b-lg opacity-30"
style={{ backgroundColor: tag.color }}
/>
</div>
);
})}
</div>
);
}

106
data/README.md Executable file
View File

@@ -0,0 +1,106 @@
# 📁 Dossier Data - TowerControl
Ce dossier contient toutes les données persistantes de l'application TowerControl.
## 📋 Structure
```
data/
├── README.md # Ce fichier
├── prod.db # Base de données production (Docker)
├── dev.db # Base de données développement (Docker)
└── backups/ # Sauvegardes automatiques et manuelles
├── towercontrol_2025-01-15T10-30-00-000Z.db.gz
├── towercontrol_2025-01-15T11-30-00-000Z.db.gz
└── ...
```
## 🎯 Utilisation
### En développement local
- La base de données principale est dans `prisma/dev.db`
- Ce dossier `data/` est utilisé uniquement par Docker
- Les sauvegardes locales sont dans `backups/` (racine du projet)
### En production Docker
- Base de données : `data/prod.db` ou `data/dev.db`
- Sauvegardes : `data/backups/`
- Tout ce dossier est mappé vers `/app/data` dans le conteneur
## 🔧 Configuration
Les chemins sont configurés via les variables d'environnement :
```bash
# Base de données
DATABASE_URL="file:../data/prod.db"
# Chemin de la base pour les backups
BACKUP_DATABASE_PATH="./data/prod.db"
# Dossier de stockage des sauvegardes
BACKUP_STORAGE_PATH="./data/backups"
```
## 🗂️ Fichiers
### Bases de données SQLite
- **prod.db** : Base de données de production
- **dev.db** : Base de données de développement Docker
- Format : SQLite 3
- Contient : Tasks, Tags, User Preferences, Sync Logs, etc.
### Sauvegardes
- **Format** : `towercontrol_YYYY-MM-DDTHH-mm-ss-sssZ.db.gz`
- **Compression** : gzip
- **Rétention** : Configurable (défaut: 5 sauvegardes)
- **Fréquence** : Configurable (défaut: horaire)
## 🚀 Commandes utiles
```bash
# Créer une sauvegarde manuelle
pnpm run backup:create
# Lister les sauvegardes
pnpm run backup:list
# Voir la configuration
pnpm run backup:config
# Restaurer une sauvegarde (dev uniquement)
pnpm run backup:restore filename.db.gz
```
## ⚠️ Important
- **Ne pas modifier** les fichiers `.db` directement
- **Ne pas supprimer** ce dossier en production
- **Sauvegarder régulièrement** le contenu de ce dossier
- **Vérifier l'espace disque** disponible pour les sauvegardes
## 🔒 Sécurité
- Ce dossier est ignoré par Git (`.gitignore`)
- Contient des données sensibles en production
- Accès restreint recommandé sur le serveur
- Chiffrement recommandé pour les sauvegardes externes
## 📊 Monitoring
Pour surveiller l'espace disque :
```bash
# Taille du dossier data
du -sh data/
# Taille des sauvegardes
du -sh data/backups/
# Nombre de sauvegardes
ls -1 data/backups/ | wc -l
```

0
dev.db
View File

View File

@@ -1,53 +1,76 @@
version: '3.8'
services:
towercontrol:
build:
context: .
dockerfile: Dockerfile
target: runner
ports:
- "3006:3000"
- '${PORT:-3007}:3000'
environment:
- NODE_ENV=production
- DATABASE_URL=file:/app/data/prod.db
- TZ=Europe/Paris
NODE_ENV: ${NODE_ENV:-production}
DATABASE_URL: ${DATABASE_URL:-file:/app/data/dev.db}
BACKUP_DATABASE_PATH: ${BACKUP_DATABASE_PATH:-./data/dev.db}
BACKUP_STORAGE_PATH: ${BACKUP_STORAGE_PATH:-./data/backups}
TZ: ${TZ:-Europe/Paris}
# NextAuth.js
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-TbwIWAmQgBcOlg7jRZrhkeEUDTpSr8Cj/Cc7W58fAyw=}
NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:3006}
# Jira (optionnel)
JIRA_BASE_URL: ${JIRA_BASE_URL:-}
JIRA_EMAIL: ${JIRA_EMAIL:-}
JIRA_API_TOKEN: ${JIRA_API_TOKEN:-}
# Debug
VERBOSE_LOGGING: ${VERBOSE_LOGGING:-false}
volumes:
# Volume persistant pour la base SQLite
- sqlite_data:/app/data
# Monter ta DB locale (décommente pour utiliser tes données locales)
- ./prisma/dev.db:/app/data/prod.db
- ./backups:/app/backups
- ./data:/app/data # Dossier local data/ vers /app/data
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health || exit 1"]
test: ['CMD', 'wget', '-qO-', 'http://localhost:3000/api/health']
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# Service de développement (optionnel)
towercontrol-dev:
build:
context: .
dockerfile: Dockerfile
target: base
ports:
- "3005:3000"
- '${PORT_DEV:-3005}:3000'
environment:
- NODE_ENV=development
- DATABASE_URL=file:/app/data/dev.db
NODE_ENV: ${NODE_ENV:-development}
DATABASE_URL: ${DATABASE_URL:-file:/app/data/dev.db}
BACKUP_DATABASE_PATH: ${BACKUP_DATABASE_PATH:-./data/dev.db}
BACKUP_STORAGE_PATH: ${BACKUP_STORAGE_PATH:-./data/backups}
TZ: ${TZ:-Europe/Paris}
# NextAuth.js
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-TbwIWAmQgBcOlg7jRZrhkeEUDTpSr8Cj/Cc7W58fAyw=}
NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:3005}
# Jira (optionnel)
JIRA_BASE_URL: ${JIRA_BASE_URL:-}
JIRA_EMAIL: ${JIRA_EMAIL:-}
JIRA_API_TOKEN: ${JIRA_API_TOKEN:-}
# Debug
VERBOSE_LOGGING: ${VERBOSE_LOGGING:-false}
volumes:
- .:/app
- /app/node_modules
- .:/app # code en live
- /app/node_modules # vol anonyme pour ne pas écraser ceux du conteneur
- /app/.next
- sqlite_data_dev:/app/data
command: sh -c "npm install && npx prisma generate && npx prisma migrate deploy && npm run dev"
- ./data:/app/data # Dossier local data/ vers /app/data
command: >
sh -c "pnpm install &&
pnpm prisma generate &&
(pnpm prisma migrate deploy || (echo 'Migration failed, using db push for fresh database...' && pnpm prisma db push --accept-data-loss --skip-generate && for migration in prisma/migrations/*/; do if [ -d \"\$migration\" ] && [ -f \"\$migration/migration.sql\" ]; then migration_name=\$(basename \"\$migration\"); pnpm prisma migrate resolve --applied \"\$migration_name\" 2>/dev/null || true; fi; done)) &&
pnpm run dev"
profiles:
- dev
volumes:
sqlite_data:
driver: local
sqlite_data_dev:
driver: local
# 📁 Structure des données :
# ./data/ -> /app/data (bind mount)
# ├── prod.db -> Base de données production
# ├── dev.db -> Base de données développement
# └── backups/ -> Sauvegardes automatiques
#
# 🔧 Configuration via variables d'environnement (.env ou .env.local)
# Les variables utilisent la syntaxe ${VAR:-default} pour les fallbacks
# 📚 Documentation : ./data/README.md et env.example

View File

@@ -1,11 +1,23 @@
# Base de données (requis)
DATABASE_URL="file:./dev.db"
DATABASE_URL="file:../data/dev.db"
# Chemin de la base de données pour les backups (optionnel)
# Si non défini, utilise DATABASE_URL ou le chemin par défaut
BACKUP_DATABASE_PATH="./data/dev.db"
# Dossier de stockage des sauvegardes (optionnel)
# Par défaut: ./backups en local, ./data/backups en production
BACKUP_STORAGE_PATH="./backups"
# Intégration Jira (optionnel)
JIRA_BASE_URL="" # https://votre-domaine.atlassian.net
JIRA_EMAIL="" # votre.email@domaine.com
JIRA_API_TOKEN="" # Token API Jira
# NextAuth (requis)
NEXTAUTH_URL="http://localhost:3000" # URL de votre application
NEXTAUTH_SECRET="your-secret-key-here" # Clé secrète pour signer les tokens
# Debug (optionnel)
VERBOSE_LOGGING="false" # Logs détaillés en développement
NODE_ENV="development" # development | production

View File

@@ -1,6 +1,6 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import { FlatCompat } from '@eslint/eslintrc';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -10,14 +10,16 @@ const compat = new FlatCompat({
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
...compat.extends('next/core-web-vitals', 'next/typescript'),
{
ignores: [
"node_modules/**",
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
'node_modules/**',
'.next/**',
'out/**',
'build/**',
'next-env.d.ts',
'scripts/test-runner.js', // Script Node.js qui utilise require() légitimement
'scripts/generate-icons-from-jpg.ts', // Script utilitaire avec require()
],
},
];

View File

@@ -1,468 +0,0 @@
'use client';
import { useState, useEffect, useCallback, useTransition } from 'react';
import { dailyClient, DailyHistoryFilters, DailySearchFilters, ReorderCheckboxesData } from '@/clients/daily-client';
import { DailyView, DailyCheckbox, UpdateDailyCheckboxData, DailyCheckboxType } from '@/lib/types';
import {
toggleCheckbox as toggleCheckboxAction,
addTodayCheckbox as addTodayCheckboxAction,
addYesterdayCheckbox as addYesterdayCheckboxAction,
updateCheckbox as updateCheckboxAction,
deleteCheckbox as deleteCheckboxAction,
reorderCheckboxes as reorderCheckboxesAction
} from '@/actions/daily';
interface UseDailyState {
dailyView: DailyView | null;
loading: boolean;
refreshing: boolean; // Pour les refresh silencieux
error: string | null;
saving: boolean; // Pour indiquer les opérations en cours
isPending: boolean; // Pour indiquer les server actions en cours
}
interface UseDailyActions {
refreshDaily: () => Promise<void>;
refreshDailySilent: () => Promise<void>;
addTodayCheckbox: (text: string, type?: DailyCheckboxType, taskId?: string) => Promise<DailyCheckbox | null>;
addYesterdayCheckbox: (text: string, type?: DailyCheckboxType, taskId?: string) => Promise<DailyCheckbox | null>;
updateCheckbox: (checkboxId: string, data: UpdateDailyCheckboxData) => Promise<DailyCheckbox | null>;
deleteCheckbox: (checkboxId: string) => Promise<void>;
toggleCheckbox: (checkboxId: string) => Promise<void>;
toggleAllToday: () => Promise<void>;
toggleAllYesterday: () => Promise<void>;
reorderCheckboxes: (data: ReorderCheckboxesData) => Promise<void>;
goToPreviousDay: () => Promise<void>;
goToNextDay: () => Promise<void>;
goToToday: () => Promise<void>;
setDate: (date: Date) => Promise<void>;
}
/**
* Hook pour la gestion d'une vue daily spécifique
*/
export function useDaily(initialDate?: Date, initialDailyView?: DailyView): UseDailyState & UseDailyActions & { currentDate: Date } {
const [currentDate, setCurrentDate] = useState<Date>(initialDate || new Date());
const [dailyView, setDailyView] = useState<DailyView | null>(initialDailyView || null);
const [loading, setLoading] = useState(!initialDailyView); // Pas de loading si on a des données SSR
const [refreshing, setRefreshing] = useState(false); // Pour les refresh silencieux
const [error, setError] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
const [isPending, startTransition] = useTransition();
const refreshDaily = useCallback(async () => {
try {
setLoading(true);
setError(null);
const view = await dailyClient.getDailyView(currentDate);
setDailyView(view);
} catch (err) {
setError(err instanceof Error ? err.message : 'Erreur lors du chargement du daily');
console.error('Erreur refreshDaily:', err);
} finally {
setLoading(false);
}
}, [currentDate]);
const refreshDailySilent = useCallback(async () => {
try {
setRefreshing(true);
// Refresh silencieux sans setLoading(true) pour éviter le clignotement
const view = await dailyClient.getDailyView(currentDate);
setDailyView(view);
} catch (err) {
console.error('Erreur refreshDailySilent:', err);
// On n'affiche pas l'erreur pour ne pas perturber l'UX
} finally {
setRefreshing(false);
}
}, [currentDate]);
const addTodayCheckbox = useCallback((text: string, type?: DailyCheckboxType, taskId?: string): Promise<DailyCheckbox | null> => {
if (!dailyView) return Promise.resolve(null);
return new Promise((resolve) => {
startTransition(async () => {
try {
setError(null);
const result = await addTodayCheckboxAction(text, type, taskId);
if (result.success && result.data) {
// Mise à jour optimiste
setDailyView(prev => prev ? {
...prev,
today: [...prev.today, result.data!].sort((a, b) => a.order - b.order)
} : null);
resolve(result.data);
} else {
setError(result.error || 'Erreur lors de l\'ajout de la checkbox');
resolve(null);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Erreur lors de l\'ajout de la checkbox');
console.error('Erreur addTodayCheckbox:', err);
resolve(null);
}
});
});
}, [dailyView]);
const addYesterdayCheckbox = useCallback((text: string, type?: DailyCheckboxType, taskId?: string): Promise<DailyCheckbox | null> => {
if (!dailyView) return Promise.resolve(null);
return new Promise((resolve) => {
startTransition(async () => {
try {
setError(null);
const result = await addYesterdayCheckboxAction(text, type, taskId);
if (result.success && result.data) {
// Mise à jour optimiste
setDailyView(prev => prev ? {
...prev,
yesterday: [...prev.yesterday, result.data!].sort((a, b) => a.order - b.order)
} : null);
resolve(result.data);
} else {
setError(result.error || 'Erreur lors de l\'ajout de la checkbox');
resolve(null);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Erreur lors de l\'ajout de la checkbox');
console.error('Erreur addYesterdayCheckbox:', err);
resolve(null);
}
});
});
}, [dailyView]);
const updateCheckbox = useCallback((checkboxId: string, data: UpdateDailyCheckboxData): Promise<DailyCheckbox | null> => {
if (!dailyView) return Promise.resolve(null);
return new Promise((resolve) => {
startTransition(async () => {
try {
setError(null);
const result = await updateCheckboxAction(checkboxId, data);
if (result.success && result.data) {
// Mise à jour optimiste
setDailyView(prev => prev ? {
...prev,
yesterday: prev.yesterday.map(cb => cb.id === checkboxId ? result.data! : cb),
today: prev.today.map(cb => cb.id === checkboxId ? result.data! : cb)
} : null);
resolve(result.data);
} else {
setError(result.error || 'Erreur lors de la mise à jour de la checkbox');
resolve(null);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Erreur lors de la mise à jour de la checkbox');
console.error('Erreur updateCheckbox:', err);
resolve(null);
}
});
});
}, [dailyView]);
const deleteCheckbox = useCallback((checkboxId: string): Promise<void> => {
if (!dailyView) return Promise.resolve();
return new Promise((resolve) => {
startTransition(async () => {
const previousDailyView = dailyView;
// Mise à jour optimiste IMMÉDIATE - supprimer la checkbox
setDailyView(prev => prev ? {
...prev,
yesterday: prev.yesterday.filter(cb => cb.id !== checkboxId),
today: prev.today.filter(cb => cb.id !== checkboxId)
} : null);
try {
const result = await deleteCheckboxAction(checkboxId);
if (!result.success) {
// Rollback en cas d'erreur
setDailyView(previousDailyView);
setError(result.error || 'Erreur lors de la suppression de la checkbox');
}
resolve();
} catch (err) {
// Rollback en cas d'erreur
setDailyView(previousDailyView);
setError(err instanceof Error ? err.message : 'Erreur lors de la suppression de la checkbox');
console.error('Erreur deleteCheckbox:', err);
resolve();
}
});
});
}, [dailyView]);
const toggleCheckbox = useCallback((checkboxId: string): Promise<void> => {
if (!dailyView) return Promise.resolve();
return new Promise((resolve) => {
startTransition(async () => {
// Trouver la checkbox dans yesterday ou today
let checkbox = dailyView.yesterday.find(cb => cb.id === checkboxId);
if (!checkbox) {
checkbox = dailyView.today.find(cb => cb.id === checkboxId);
}
if (!checkbox) {
resolve();
return;
}
// Mise à jour optimiste IMMÉDIATE
const newCheckedState = !checkbox.isChecked;
const previousDailyView = dailyView;
setDailyView(prev => prev ? {
...prev,
yesterday: prev.yesterday.map(cb =>
cb.id === checkboxId ? { ...cb, isChecked: newCheckedState } : cb
),
today: prev.today.map(cb =>
cb.id === checkboxId ? { ...cb, isChecked: newCheckedState } : cb
)
} : null);
try {
const result = await toggleCheckboxAction(checkboxId);
if (!result.success) {
// Rollback en cas d'erreur
setDailyView(previousDailyView);
setError(result.error || 'Erreur lors de la mise à jour de la checkbox');
}
resolve();
} catch (err) {
// Rollback en cas d'erreur
setDailyView(previousDailyView);
setError(err instanceof Error ? err.message : 'Erreur lors de la mise à jour de la checkbox');
console.error('Erreur toggleCheckbox:', err);
resolve();
}
});
});
}, [dailyView]);
const toggleAllToday = useCallback(async (): Promise<void> => {
if (!dailyView) return;
const todayCheckboxes = dailyView.today;
if (todayCheckboxes.length === 0) return;
// Déterminer si on coche tout ou on décoche tout
const allChecked = todayCheckboxes.every(cb => cb.isChecked);
const newCheckedState = !allChecked;
const previousDailyView = dailyView;
// Mise à jour optimiste IMMÉDIATE
setDailyView(prev => prev ? {
...prev,
today: prev.today.map(cb => ({ ...cb, isChecked: newCheckedState }))
} : null);
try {
// Appeler l'API pour chaque checkbox en parallèle
await Promise.all(
todayCheckboxes.map(checkbox =>
updateCheckboxAction(checkbox.id, { isChecked: newCheckedState })
)
);
} catch (err) {
// Rollback en cas d'erreur
setDailyView(previousDailyView);
setError(err instanceof Error ? err.message : 'Erreur lors de la mise à jour des checkboxes');
console.error('Erreur toggleAllToday:', err);
}
}, [dailyView]);
const toggleAllYesterday = useCallback(async (): Promise<void> => {
if (!dailyView) return;
const yesterdayCheckboxes = dailyView.yesterday;
if (yesterdayCheckboxes.length === 0) return;
// Déterminer si on coche tout ou on décoche tout
const allChecked = yesterdayCheckboxes.every(cb => cb.isChecked);
const newCheckedState = !allChecked;
const previousDailyView = dailyView;
// Mise à jour optimiste IMMÉDIATE
setDailyView(prev => prev ? {
...prev,
yesterday: prev.yesterday.map(cb => ({ ...cb, isChecked: newCheckedState }))
} : null);
try {
// Appeler l'API pour chaque checkbox en parallèle
await Promise.all(
yesterdayCheckboxes.map(checkbox =>
updateCheckboxAction(checkbox.id, { isChecked: newCheckedState })
)
);
} catch (err) {
// Rollback en cas d'erreur
setDailyView(previousDailyView);
setError(err instanceof Error ? err.message : 'Erreur lors de la mise à jour des checkboxes');
console.error('Erreur toggleAllYesterday:', err);
}
}, [dailyView]);
const reorderCheckboxes = useCallback(async (data: ReorderCheckboxesData): Promise<void> => {
try {
setSaving(true);
setError(null);
// Convertir la date en string format YYYY-MM-DD pour la server action
const dailyId = data.date.toISOString().split('T')[0];
const result = await reorderCheckboxesAction(dailyId, data.checkboxIds);
if (!result.success) {
setError(result.error || 'Erreur lors du réordonnancement');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Erreur lors du réordonnancement');
console.error('Erreur reorderCheckboxes:', err);
} finally {
setSaving(false);
}
}, []);
const goToPreviousDay = useCallback(async (): Promise<void> => {
const previousDay = new Date(currentDate);
previousDay.setDate(previousDay.getDate() - 1);
setCurrentDate(previousDay);
}, [currentDate]);
const goToNextDay = useCallback(async (): Promise<void> => {
const nextDay = new Date(currentDate);
nextDay.setDate(nextDay.getDate() + 1);
setCurrentDate(nextDay);
}, [currentDate]);
const goToToday = useCallback(async (): Promise<void> => {
setCurrentDate(new Date());
}, []);
const setDate = useCallback(async (date: Date): Promise<void> => {
setCurrentDate(date);
}, []);
// État pour savoir si c'est le premier chargement
const [isInitialLoad, setIsInitialLoad] = useState(!initialDailyView);
// Charger le daily quand la date change
useEffect(() => {
if (isInitialLoad) {
// Premier chargement : utiliser refreshDaily normal seulement si pas de données SSR
if (!initialDailyView) {
refreshDaily().finally(() => setIsInitialLoad(false));
} else {
setIsInitialLoad(false);
}
} else {
// Changements suivants : utiliser refreshDailySilent
refreshDailySilent();
}
}, [refreshDaily, refreshDailySilent, isInitialLoad, initialDailyView]);
return {
// State
dailyView,
loading,
refreshing,
error,
saving,
isPending,
currentDate,
// Actions
refreshDaily,
refreshDailySilent,
addTodayCheckbox,
addYesterdayCheckbox,
updateCheckbox,
deleteCheckbox,
toggleCheckbox,
toggleAllToday,
toggleAllYesterday,
reorderCheckboxes,
goToPreviousDay,
goToNextDay,
goToToday,
setDate
};
}
/**
* Hook pour l'historique des checkboxes
*/
export function useDailyHistory() {
const [history, setHistory] = useState<{ date: Date; checkboxes: DailyCheckbox[] }[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const loadHistory = useCallback(async (filters?: DailyHistoryFilters) => {
try {
setLoading(true);
setError(null);
const historyData = await dailyClient.getCheckboxHistory(filters);
setHistory(historyData);
} catch (err) {
setError(err instanceof Error ? err.message : 'Erreur lors du chargement de l\'historique');
console.error('Erreur loadHistory:', err);
} finally {
setLoading(false);
}
}, []);
const searchCheckboxes = useCallback(async (filters: DailySearchFilters) => {
try {
setLoading(true);
setError(null);
const checkboxes = await dailyClient.searchCheckboxes(filters);
// Grouper par date pour l'affichage
const groupedHistory = checkboxes.reduce((acc, checkbox) => {
const dateKey = checkbox.date.toDateString();
const existing = acc.find(item => item.date.toDateString() === dateKey);
if (existing) {
existing.checkboxes.push(checkbox);
} else {
acc.push({ date: checkbox.date, checkboxes: [checkbox] });
}
return acc;
}, [] as { date: Date; checkboxes: DailyCheckbox[] }[]);
setHistory(groupedHistory);
} catch (err) {
setError(err instanceof Error ? err.message : 'Erreur lors de la recherche');
console.error('Erreur searchCheckboxes:', err);
} finally {
setLoading(false);
}
}, []);
return {
history,
loading,
error,
loadHistory,
searchCheckboxes
};
}

View File

@@ -1,98 +0,0 @@
import { useState, useEffect, useCallback } from 'react';
import { getAvailableJiraFilters, getFilteredJiraAnalytics } from '@/actions/jira-filters';
import { AvailableFilters, JiraAnalyticsFilters, JiraAnalytics } from '@/lib/types';
import { JiraAdvancedFiltersService } from '@/services/jira-advanced-filters';
export function useJiraFilters() {
const [availableFilters, setAvailableFilters] = useState<AvailableFilters>({
components: [],
fixVersions: [],
issueTypes: [],
statuses: [],
assignees: [],
labels: [],
priorities: []
});
const [activeFilters, setActiveFilters] = useState<Partial<JiraAnalyticsFilters>>(
JiraAdvancedFiltersService.createEmptyFilters()
);
const [filteredAnalytics, setFilteredAnalytics] = useState<JiraAnalytics | null>(null);
const [isLoadingFilters, setIsLoadingFilters] = useState(false);
const [isLoadingAnalytics, setIsLoadingAnalytics] = useState(false);
const [error, setError] = useState<string | null>(null);
// Charger les filtres disponibles
const loadAvailableFilters = useCallback(async () => {
setIsLoadingFilters(true);
setError(null);
try {
const result = await getAvailableJiraFilters();
if (result.success && result.data) {
setAvailableFilters(result.data);
} else {
setError(result.error || 'Erreur lors du chargement des filtres');
}
} catch {
setError('Erreur de connexion');
} finally {
setIsLoadingFilters(false);
}
}, []);
// Appliquer les filtres et récupérer les analytics filtrées
const applyFilters = useCallback(async (filters: Partial<JiraAnalyticsFilters>) => {
setIsLoadingAnalytics(true);
setError(null);
try {
const result = await getFilteredJiraAnalytics(filters);
if (result.success && result.data) {
setFilteredAnalytics(result.data);
setActiveFilters(filters);
} else {
setError(result.error || 'Erreur lors du filtrage');
}
} catch {
setError('Erreur de connexion');
} finally {
setIsLoadingAnalytics(false);
}
}, []);
// Effacer tous les filtres
const clearFilters = useCallback(() => {
const emptyFilters = JiraAdvancedFiltersService.createEmptyFilters();
setActiveFilters(emptyFilters);
setFilteredAnalytics(null);
}, []);
// Chargement initial des filtres disponibles
useEffect(() => {
loadAvailableFilters();
}, [loadAvailableFilters]);
return {
// État
availableFilters,
activeFilters,
filteredAnalytics,
isLoadingFilters,
isLoadingAnalytics,
error,
// Actions
loadAvailableFilters,
applyFilters,
clearFilters,
// Helpers
hasActiveFilters: JiraAdvancedFiltersService.hasActiveFilters(activeFilters),
activeFiltersCount: JiraAdvancedFiltersService.countActiveFilters(activeFilters),
filtersSummary: JiraAdvancedFiltersService.getFiltersSummary(activeFilters)
};
}

View File

@@ -1,206 +0,0 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { tasksClient, TaskFilters, CreateTaskData } from '@/clients/tasks-client';
import { updateTaskStatus, createTask as createTaskAction } from '@/actions/tasks';
import { Task, TaskStats, TaskStatus } from '@/lib/types';
interface UseTasksState {
tasks: Task[];
stats: TaskStats;
loading: boolean;
error: string | null;
syncing: boolean; // Pour indiquer les opérations optimistes en cours
}
interface UseTasksActions {
refreshTasks: () => Promise<void>;
createTask: (data: CreateTaskData) => Promise<Task | null>;
updateTaskOptimistic: (taskId: string, status: TaskStatus) => Promise<Task | null>;
setFilters: (filters: TaskFilters) => void;
}
/**
* Hook pour la gestion des tâches
*/
export function useTasks(
initialFilters?: TaskFilters,
initialData?: { tasks: Task[]; stats?: TaskStats }
): UseTasksState & UseTasksActions {
const [state, setState] = useState<UseTasksState>({
tasks: initialData?.tasks || [],
stats: initialData?.stats || {
total: 0,
completed: 0,
inProgress: 0,
todo: 0,
backlog: 0,
cancelled: 0,
freeze: 0,
archived: 0,
completionRate: 0
},
loading: false,
error: null,
syncing: false
});
const [filters, setFilters] = useState<TaskFilters>(initialFilters || {});
/**
* Récupère les tâches depuis l'API
*/
const refreshTasks = useCallback(async () => {
setState(prev => ({ ...prev, loading: true, error: null }));
try {
const response = await tasksClient.getTasks({ ...filters, limit: undefined });
setState(prev => ({
...prev,
tasks: response.data,
stats: response.stats,
loading: false
}));
} catch (error) {
setState(prev => ({
...prev,
loading: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
}));
}
}, [filters]);
/**
* Crée une nouvelle tâche
*/
const createTask = useCallback(async (data: CreateTaskData): Promise<Task | null> => {
setState(prev => ({ ...prev, loading: true, error: null }));
try {
const result = await createTaskAction({
title: data.title,
description: data.description,
status: data.status,
priority: data.priority,
tags: data.tags
});
if (result.success) {
// Rafraîchir la liste après création
await refreshTasks();
return result.data as Task;
} else {
throw new Error(result.error || 'Erreur lors de la création');
}
} catch (error) {
setState(prev => ({
...prev,
loading: false,
error: error instanceof Error ? error.message : 'Erreur lors de la création'
}));
return null;
}
}, [refreshTasks]);
// Note: updateTask et deleteTask ont été migrés vers Server Actions
// Voir /src/actions/tasks.ts pour updateTaskTitle, updateTaskStatus, deleteTask
/**
* Met à jour le statut d'une tâche de manière optimiste (pour drag & drop)
*/
const updateTaskOptimistic = useCallback(async (taskId: string, status: TaskStatus): Promise<Task | null> => {
// 1. Sauvegarder l'état actuel pour rollback
const currentTasks = state.tasks;
const taskToUpdate = currentTasks.find(t => t.id === taskId);
if (!taskToUpdate) {
console.error('Tâche non trouvée pour mise à jour optimiste:', taskId);
return null;
}
// 2. Mise à jour optimiste immédiate de l'état local
const updatedTask = { ...taskToUpdate, status };
const updatedTasks = currentTasks.map(task =>
task.id === taskId ? updatedTask : task
);
// Recalculer les stats
const newStats = {
total: updatedTasks.length,
completed: updatedTasks.filter(t => t.status === 'done').length,
inProgress: updatedTasks.filter(t => t.status === 'in_progress').length,
todo: updatedTasks.filter(t => t.status === 'todo').length,
backlog: updatedTasks.filter(t => t.status === 'backlog').length,
cancelled: updatedTasks.filter(t => t.status === 'cancelled').length,
freeze: updatedTasks.filter(t => t.status === 'freeze').length,
archived: updatedTasks.filter(t => t.status === 'archived').length,
completionRate: updatedTasks.length > 0
? Math.round((updatedTasks.filter(t => t.status === 'done').length / updatedTasks.length) * 100)
: 0
};
setState(prev => ({
...prev,
tasks: updatedTasks,
stats: newStats,
error: null,
syncing: true // Indiquer qu'une synchronisation est en cours
}));
// 3. Appel Server Action en arrière-plan
try {
const result = await updateTaskStatus(taskId, status);
// Si l'action réussit, la revalidation automatique se charge du reste
if (result.success) {
setState(prev => ({ ...prev, syncing: false }));
return result.data as Task;
} else {
throw new Error(result.error || 'Erreur lors de la mise à jour');
}
} catch (error) {
// 4. Rollback en cas d'erreur
setState(prev => ({
...prev,
tasks: currentTasks,
stats: {
total: currentTasks.length,
completed: currentTasks.filter(t => t.status === 'done').length,
inProgress: currentTasks.filter(t => t.status === 'in_progress').length,
todo: currentTasks.filter(t => t.status === 'todo').length,
backlog: currentTasks.filter(t => t.status === 'backlog').length,
cancelled: currentTasks.filter(t => t.status === 'cancelled').length,
freeze: currentTasks.filter(t => t.status === 'freeze').length,
archived: currentTasks.filter(t => t.status === 'archived').length,
completionRate: currentTasks.length > 0
? Math.round((currentTasks.filter(t => t.status === 'done').length / currentTasks.length) * 100)
: 0
},
error: error instanceof Error ? error.message : 'Erreur lors de la mise à jour',
syncing: false // Arrêter l'indicateur de synchronisation
}));
console.error('Erreur lors de la mise à jour optimiste:', error);
return null;
}
}, [state.tasks]);
// Note: deleteTask a été migré vers Server Actions
// Utilisez directement deleteTask depuis /src/actions/tasks.ts dans les composants
// Charger les tâches au montage seulement si pas de données initiales
useEffect(() => {
if (!initialData?.tasks?.length) {
refreshTasks();
}
}, [refreshTasks, initialData?.tasks?.length]);
return {
...state,
refreshTasks,
createTask,
updateTaskOptimistic,
setFilters
};
}

View File

@@ -1,11 +1,52 @@
import type { NextConfig } from "next";
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
output: 'standalone',
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'media.licdn.com',
port: '',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'avatars.githubusercontent.com',
port: '',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'lh3.googleusercontent.com',
port: '',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'cdn.discordapp.com',
port: '',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'images.unsplash.com',
port: '',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'via.placeholder.com',
port: '',
pathname: '/**',
},
],
},
turbopack: {
root: process.cwd(),
rules: {
'*.sql': ['raw'],
}
},
},
};

9209
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,50 +1,86 @@
{
"name": "towercontrol",
"version": "0.1.0",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build --turbopack",
"build": "prisma generate && next build --turbopack",
"start": "next start",
"postinstall": "prisma generate",
"lint": "eslint",
"backup:create": "npx tsx scripts/backup-manager.ts create",
"backup:list": "npx tsx scripts/backup-manager.ts list",
"backup:verify": "npx tsx scripts/backup-manager.ts verify",
"backup:config": "npx tsx scripts/backup-manager.ts config",
"backup:start": "npx tsx scripts/backup-manager.ts scheduler-start",
"backup:stop": "npx tsx scripts/backup-manager.ts scheduler-stop",
"backup:status": "npx tsx scripts/backup-manager.ts scheduler-status"
"backup:create": "pnpm tsx scripts/backup-manager.ts create",
"backup:list": "pnpm tsx scripts/backup-manager.ts list",
"backup:verify": "pnpm tsx scripts/backup-manager.ts verify",
"backup:config": "pnpm tsx scripts/backup-manager.ts config",
"backup:start": "pnpm tsx scripts/backup-manager.ts scheduler-start",
"backup:stop": "pnpm tsx scripts/backup-manager.ts scheduler-stop",
"backup:status": "pnpm tsx scripts/backup-manager.ts scheduler-status",
"cache:monitor": "pnpm tsx scripts/cache-monitor.ts",
"cache:stats": "pnpm tsx scripts/cache-monitor.ts stats",
"cache:cleanup": "pnpm tsx scripts/cache-monitor.ts cleanup",
"cache:clear": "pnpm tsx scripts/cache-monitor.ts clear",
"test": "node scripts/test-runner.js",
"test:watch": "vitest --watch --reporter=verbose",
"test:coverage": "vitest --coverage --reporter=verbose",
"test:ui": "vitest --ui",
"test:story-points": "pnpm tsx scripts/test-story-points.ts",
"test:jira-fields": "pnpm tsx scripts/test-jira-fields.ts",
"prettier:format": "prettier --write .",
"prettier:check": "prettier --check .",
"prepare": "husky"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emoji-mart/data": "^1.2.1",
"@emoji-mart/react": "^1.1.1",
"@prisma/client": "^6.16.1",
"@types/jspdf": "^1.3.3",
"bcryptjs": "^3.0.2",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"jspdf": "^3.0.3",
"emoji-mart": "^5.6.0",
"emoji-regex": "^10.5.0",
"lucide-react": "^0.544.0",
"mermaid": "^11.12.0",
"next": "15.5.3",
"next-auth": "^4.24.11",
"prism-react-renderer": "^2.4.1",
"prisma": "^6.16.1",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-markdown": "^10.1.0",
"recharts": "^3.2.1",
"sqlite3": "^5.1.7",
"tailwind-merge": "^3.3.1"
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.1",
"remark-toc": "^9.0.0",
"tailwind-merge": "^3.3.1",
"twemoji": "^14.0.2"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/bcryptjs": "^2.4.6",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.5.3",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"eslint-config-next": "^15.5.3",
"husky": "^9.1.7",
"knip": "^5.64.0",
"lint-staged": "^15.5.2",
"prettier": "^3.6.2",
"tailwindcss": "^4",
"sharp": "^0.34.5",
"tailwindcss": "^4.1.14",
"tsx": "^4.19.2",
"typescript": "^5"
"typescript": "^5",
"vitest": "^2.1.8"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,json,css,md}": [
"prettier --write"
]
}
}

8212
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
plugins: ['@tailwindcss/postcss'],
};
export default config;

View File

@@ -0,0 +1,59 @@
-- Migration pour ajouter ownerId aux tags
-- Les tags existants seront assignés au premier utilisateur
-- Cette version préserve les relations TaskTag existantes
-- Étape 1: Ajouter la colonne ownerId temporairement nullable
ALTER TABLE "tags" ADD COLUMN "ownerId" TEXT;
-- Étape 2: Assigner tous les tags existants au premier utilisateur
UPDATE "tags"
SET "ownerId" = (
SELECT "id" FROM "users"
ORDER BY "createdAt" ASC
LIMIT 1
)
WHERE "ownerId" IS NULL;
-- Étape 3: Sauvegarder les relations TaskTag existantes avec les noms des tags
CREATE TEMPORARY TABLE "temp_task_tag_names" AS
SELECT tt."taskId", t."name" as "tagName"
FROM "task_tags" tt
JOIN "tags" t ON tt."tagId" = t."id";
-- Étape 4: Supprimer les anciennes relations TaskTag
DELETE FROM "task_tags";
-- Étape 5: Créer la nouvelle table avec ownerId non-nullable
CREATE TABLE "new_tags" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"color" TEXT NOT NULL DEFAULT '#6b7280',
"isPinned" BOOLEAN NOT NULL DEFAULT false,
"ownerId" TEXT NOT NULL,
CONSTRAINT "new_tags_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- Étape 6: Copier les données des tags
INSERT INTO "new_tags" ("id", "name", "color", "isPinned", "ownerId")
SELECT "id", "name", "color", "isPinned", "ownerId" FROM "tags";
-- Étape 7: Supprimer l'ancienne table
DROP TABLE "tags";
-- Étape 8: Renommer la nouvelle table
ALTER TABLE "new_tags" RENAME TO "tags";
-- Étape 9: Créer l'index unique pour (name, ownerId)
CREATE UNIQUE INDEX "tags_name_ownerId_key" ON "tags"("name", "ownerId");
-- Étape 10: Restaurer les relations TaskTag en utilisant les noms des tags
INSERT INTO "task_tags" ("taskId", "tagId")
SELECT tt."taskId", t."id" as "tagId"
FROM "temp_task_tag_names" tt
JOIN "tags" t ON tt."tagName" = t."name"
WHERE EXISTS (
SELECT 1 FROM "tasks" WHERE "tasks"."id" = tt."taskId"
);
-- Étape 11: Nettoyer la table temporaire
DROP TABLE "temp_task_tag_names";

View File

@@ -0,0 +1,23 @@
-- Migration pour ajouter userId aux UserPreferences
-- et migrer les données existantes vers le premier utilisateur
-- 1. Ajouter la colonne userId (nullable temporairement)
ALTER TABLE "user_preferences" ADD COLUMN "userId" TEXT;
-- 2. Créer un index unique sur userId
CREATE UNIQUE INDEX "user_preferences_userId_key" ON "user_preferences"("userId");
-- 3. Migrer les données existantes vers le premier utilisateur
-- (on suppose qu'il y a au moins un utilisateur dans la table users)
UPDATE "user_preferences"
SET "userId" = (SELECT id FROM "users" LIMIT 1)
WHERE "userId" IS NULL;
-- 4. Rendre la colonne userId non-nullable
-- Note: SQLite ne supporte pas ALTER COLUMN, donc on doit recréer la table
-- Mais comme on a déjà des données, on va juste s'assurer que toutes les entrées ont un userId
-- En production, on devrait faire une migration plus complexe
-- 5. Ajouter la contrainte de clé étrangère
-- SQLite ne supporte pas les contraintes de clé étrangère dans ALTER TABLE
-- La contrainte sera gérée par Prisma au niveau applicatif

View File

@@ -0,0 +1,6 @@
-- AlterTable
ALTER TABLE "Note" ADD COLUMN "taskId" TEXT;
-- AddForeignKey
ALTER TABLE "Note" ADD CONSTRAINT "Note_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "tasks"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -0,0 +1,54 @@
-- Add ownerId column to tasks table
ALTER TABLE "tasks" ADD COLUMN "ownerId" TEXT NOT NULL DEFAULT '';
-- Get the first user ID to assign all existing tasks
-- We'll use a subquery to get the first user's ID
UPDATE "tasks"
SET "ownerId" = (
SELECT "id" FROM "users"
ORDER BY "createdAt" ASC
LIMIT 1
)
WHERE "ownerId" = '';
-- Now make ownerId NOT NULL without default
-- First, we need to recreate the table since SQLite doesn't support ALTER COLUMN
CREATE TABLE "tasks_new" (
"id" TEXT NOT NULL PRIMARY KEY,
"title" TEXT NOT NULL,
"description" TEXT,
"status" TEXT NOT NULL DEFAULT 'todo',
"priority" TEXT NOT NULL DEFAULT 'medium',
"source" TEXT NOT NULL,
"sourceId" TEXT,
"dueDate" DATETIME,
"completedAt" DATETIME,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"jiraProject" TEXT,
"jiraKey" TEXT,
"assignee" TEXT,
"ownerId" TEXT NOT NULL,
"jiraType" TEXT,
"tfsProject" TEXT,
"tfsPullRequestId" INTEGER,
"tfsRepository" TEXT,
"tfsSourceBranch" TEXT,
"tfsTargetBranch" TEXT,
"primaryTagId" TEXT,
CONSTRAINT "tasks_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "tasks_primaryTagId_fkey" FOREIGN KEY ("primaryTagId") REFERENCES "tags" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- Copy data from old table to new table
INSERT INTO "tasks_new" SELECT * FROM "tasks";
-- Drop old table
DROP TABLE "tasks";
-- Rename new table
ALTER TABLE "tasks_new" RENAME TO "tasks";
-- Recreate indexes
CREATE UNIQUE INDEX "tasks_source_sourceId_key" ON "tasks"("source", "sourceId");
CREATE INDEX "tasks_ownerId_idx" ON "tasks"("ownerId");

View File

@@ -0,0 +1,56 @@
-- Add ownerId column to tasks table if it doesn't exist
ALTER TABLE "tasks" ADD COLUMN "ownerId" TEXT;
-- Create a temporary user if no users exist
INSERT OR IGNORE INTO "users" ("id", "email", "name", "password", "createdAt", "updatedAt")
VALUES ('temp-user', 'temp@example.com', 'Temporary User', '$2b$10$temp', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
-- Assign all existing tasks to the first user (or temp user if none exist)
UPDATE "tasks"
SET "ownerId" = (
SELECT "id" FROM "users"
ORDER BY "createdAt" ASC
LIMIT 1
)
WHERE "ownerId" IS NULL;
-- Now make ownerId NOT NULL by recreating the table
CREATE TABLE "tasks_new" (
"id" TEXT NOT NULL PRIMARY KEY,
"title" TEXT NOT NULL,
"description" TEXT,
"status" TEXT NOT NULL DEFAULT 'todo',
"priority" TEXT NOT NULL DEFAULT 'medium',
"source" TEXT NOT NULL,
"sourceId" TEXT,
"dueDate" DATETIME,
"completedAt" DATETIME,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"jiraProject" TEXT,
"jiraKey" TEXT,
"assignee" TEXT,
"ownerId" TEXT NOT NULL,
"jiraType" TEXT,
"tfsProject" TEXT,
"tfsPullRequestId" INTEGER,
"tfsRepository" TEXT,
"tfsSourceBranch" TEXT,
"tfsTargetBranch" TEXT,
"primaryTagId" TEXT,
CONSTRAINT "tasks_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "tasks_primaryTagId_fkey" FOREIGN KEY ("primaryTagId") REFERENCES "tags" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- Copy data from old table to new table
INSERT INTO "tasks_new" SELECT * FROM "tasks";
-- Drop old table
DROP TABLE "tasks";
-- Rename new table
ALTER TABLE "tasks_new" RENAME TO "tasks";
-- Recreate indexes
CREATE UNIQUE INDEX "tasks_source_sourceId_key" ON "tasks"("source", "sourceId");
CREATE INDEX "tasks_ownerId_idx" ON "tasks"("ownerId");

View File

@@ -0,0 +1,17 @@
-- Migration pour ajouter userId aux DailyCheckbox
-- et associer les entrées existantes au premier utilisateur
-- 1. Ajouter la colonne userId (nullable temporairement)
ALTER TABLE "daily_checkboxes" ADD COLUMN "userId" TEXT;
-- 2. Migrer les données existantes vers le premier utilisateur
-- (on suppose qu'il y a au moins un utilisateur dans la table users)
UPDATE "daily_checkboxes"
SET "userId" = (SELECT id FROM "users" LIMIT 1)
WHERE "userId" IS NULL;
-- 3. Créer un index sur userId pour les performances
CREATE INDEX "daily_checkboxes_userId_idx" ON "daily_checkboxes"("userId");
-- Note: La contrainte de clé étrangère sera gérée par Prisma
-- SQLite ne supporte pas les contraintes de clé étrangère dans ALTER TABLE

View File

@@ -1,6 +1,3 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
@@ -10,48 +7,81 @@ datasource db {
url = env("DATABASE_URL")
}
model Task {
id String @id @default(cuid())
title String
description String?
status String @default("todo")
priority String @default("medium")
source String // "reminders" | "jira"
sourceId String? // ID dans le système source
dueDate DateTime?
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Métadonnées Jira
jiraProject String?
jiraKey String?
jiraType String? // Type de ticket Jira: Story, Task, Bug, Epic, etc.
assignee String?
// Relations
taskTags TaskTag[]
model User {
id String @id @default(cuid())
email String @unique
name String?
firstName String?
lastName String?
avatar String? // URL de l'avatar
role String @default("user") // user, admin, etc.
isActive Boolean @default(true)
lastLoginAt DateTime?
password String // Hashé avec bcrypt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
preferences UserPreferences?
notes Note[]
dailyCheckboxes DailyCheckbox[]
tasks Task[] @relation("TaskOwner")
tags Tag[] @relation("TagOwner")
@@map("users")
}
model Task {
id String @id @default(cuid())
title String
description String?
status String @default("todo")
priority String @default("medium")
source String
sourceId String?
dueDate DateTime?
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
jiraProject String?
jiraKey String?
assignee String? // Legacy field - keep for Jira/TFS compatibility
ownerId String // Required - chaque tâche appartient à un user
owner User @relation("TaskOwner", fields: [ownerId], references: [id], onDelete: Cascade)
jiraType String?
tfsProject String?
tfsPullRequestId Int?
tfsRepository String?
tfsSourceBranch String?
tfsTargetBranch String?
primaryTagId String?
primaryTag Tag? @relation("PrimaryTag", fields: [primaryTagId], references: [id])
dailyCheckboxes DailyCheckbox[]
taskTags TaskTag[]
notes Note[] // Notes associées à cette tâche
@@unique([source, sourceId])
@@map("tasks")
}
model Tag {
id String @id @default(cuid())
name String @unique
color String @default("#6b7280")
isPinned Boolean @default(false) // Tag pour objectifs principaux
taskTags TaskTag[]
id String @id @default(cuid())
name String
color String @default("#6b7280")
isPinned Boolean @default(false)
ownerId String // Chaque tag appartient à un utilisateur
owner User @relation("TagOwner", fields: [ownerId], references: [id], onDelete: Cascade)
taskTags TaskTag[]
primaryTasks Task[] @relation("PrimaryTag")
noteTags NoteTag[]
@@unique([name, ownerId]) // Un nom de tag unique par utilisateur
@@map("tags")
}
model TaskTag {
taskId String
tagId String
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
@@id([taskId, tagId])
@@map("task_tags")
@@ -59,8 +89,8 @@ model TaskTag {
model SyncLog {
id String @id @default(cuid())
source String // "reminders" | "jira"
status String // "success" | "error"
source String
status String
message String?
tasksSync Int @default(0)
createdAt DateTime @default(now())
@@ -70,39 +100,61 @@ model SyncLog {
model DailyCheckbox {
id String @id @default(cuid())
date DateTime // Date de la checkbox (YYYY-MM-DD)
text String // Texte de la checkbox
date DateTime
text String
isChecked Boolean @default(false)
type String @default("task") // "task" | "meeting"
order Int @default(0) // Ordre d'affichage pour cette date
taskId String? // Liaison optionnelle vers une tâche
type String @default("task")
order Int @default(0)
taskId String?
userId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
task Task? @relation(fields: [taskId], references: [id], onDelete: SetNull)
task Task? @relation(fields: [taskId], references: [id])
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([date])
@@index([userId])
@@map("daily_checkboxes")
}
model UserPreferences {
id String @id @default(cuid())
// Filtres Kanban (JSON)
kanbanFilters Json?
// Préférences de vue (JSON)
viewPreferences Json?
// Visibilité des colonnes (JSON)
id String @id @default(cuid())
userId String @unique
kanbanFilters Json?
viewPreferences Json?
columnVisibility Json?
// Configuration Jira (JSON)
jiraConfig Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
jiraConfig Json?
jiraAutoSync Boolean @default(false)
jiraSyncInterval String @default("daily")
tfsConfig Json?
tfsAutoSync Boolean @default(false)
tfsSyncInterval String @default("daily")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("user_preferences")
}
model Note {
id String @id @default(cuid())
title String
content String // Markdown content
userId String
taskId String? // Tâche associée à la note
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
task Task? @relation(fields: [taskId], references: [id])
noteTags NoteTag[]
}
model NoteTag {
noteId String
tagId String
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)
@@id([noteId, tagId])
@@map("note_tags")
}

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
public/icon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 B

BIN
public/icon-180x180.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
public/icon-192x192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
public/icon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
public/icon-512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

BIN
public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
public/icons/iconTC.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

BIN
public/icons/iconTC2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
public/icons/iconTC3S.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
public/icons/iconTC4S.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
public/icons/logoTC5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
public/icons/logoTC6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
public/icons/logoTC7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
public/icons/logoTC8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 841 KiB

185
scripts/auto-version.ts Normal file
View File

@@ -0,0 +1,185 @@
import { execSync } from 'child_process';
import { readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
interface Version {
major: number;
minor: number;
patch: number;
}
function parseVersion(version: string): Version {
const [major, minor, patch] = version.split('.').map(Number);
return { major, minor, patch };
}
function formatVersion(v: Version): string {
return `${v.major}.${v.minor}.${v.patch}`;
}
function getLastVersionTag(): string | null {
try {
const tag = execSync('git describe --tags --abbrev=0', {
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
return tag;
} catch {
return null;
}
}
function getCommitsSinceTag(tag: string | null): string[] {
try {
const range = tag ? `${tag}..HEAD` : 'HEAD';
const commits = execSync(`git log ${range} --pretty=format:"%s"`, {
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'ignore'],
})
.trim()
.split('\n')
.filter(Boolean);
return commits;
} catch {
return [];
}
}
function determineVersionBump(commits: string[]): 'major' | 'minor' | 'patch' {
let hasBreaking = false;
let hasFeature = false;
let hasPatch = false;
for (const commit of commits) {
const lowerCommit = commit.toLowerCase();
// Breaking changes (major bump)
if (
lowerCommit.includes('breaking change') ||
lowerCommit.includes('breaking:') ||
lowerCommit.match(/^[a-z]+!:/) || // feat!:, refactor!:, etc.
lowerCommit.includes('!')
) {
hasBreaking = true;
}
// Features (minor bump)
if (lowerCommit.startsWith('feat:')) {
hasFeature = true;
}
// Patch bumps: fixes, performance improvements, security fixes, refactorings
if (
lowerCommit.startsWith('fix:') ||
lowerCommit.startsWith('perf:') ||
lowerCommit.startsWith('security:') ||
lowerCommit.startsWith('patch:') ||
lowerCommit.startsWith('refactor:')
) {
hasPatch = true;
}
}
if (hasBreaking) return 'major';
if (hasFeature) return 'minor';
if (hasPatch) return 'patch';
// Par défaut, patch si on a des commits mais aucun type spécifique
return commits.length > 0 ? 'patch' : 'patch';
}
function incrementVersion(
current: Version,
type: 'major' | 'minor' | 'patch'
): Version {
switch (type) {
case 'major':
return { major: current.major + 1, minor: 0, patch: 0 };
case 'minor':
return { major: current.major, minor: current.minor + 1, patch: 0 };
case 'patch':
return {
major: current.major,
minor: current.minor,
patch: current.patch + 1,
};
}
}
function main() {
const silent = process.argv.includes('--silent');
const hookMode = process.argv.includes('--hook');
try {
const packagePath = join(process.cwd(), 'package.json');
const packageJson = JSON.parse(readFileSync(packagePath, 'utf-8'));
const currentVersion = parseVersion(packageJson.version);
const lastTag = getLastVersionTag();
const commits = getCommitsSinceTag(lastTag);
if (commits.length === 0) {
if (!silent) {
console.log('✅ Aucun nouveau commit depuis la dernière version');
console.log(`Version actuelle: ${packageJson.version}`);
}
return;
}
const bumpType = determineVersionBump(commits);
const newVersion = incrementVersion(currentVersion, bumpType);
const newVersionString = formatVersion(newVersion);
// Si la version n'a pas changé, ne rien faire
if (newVersionString === packageJson.version) {
return;
}
if (!silent) {
console.log(`📊 Analyse des commits depuis ${lastTag || 'le début'}:`);
console.log(` - ${commits.length} commit(s) trouvé(s)`);
console.log(` - Type de bump détecté: ${bumpType}`);
console.log(` - Version actuelle: ${packageJson.version}`);
console.log(` - Nouvelle version: ${newVersionString}`);
// Afficher les commits pertinents
console.log('\n📝 Commits analysés:');
commits.slice(0, 10).forEach((commit) => {
console.log(` - ${commit}`);
});
if (commits.length > 10) {
console.log(` ... et ${commits.length - 10} autre(s) commit(s)`);
}
}
// Mettre à jour package.json
packageJson.version = newVersionString;
writeFileSync(packagePath, JSON.stringify(packageJson, null, 2) + '\n');
if (!silent) {
console.log(`\n✅ Version mise à jour dans package.json`);
console.log(
`\n💡 Prochaines étapes:` +
`\n 1. git add package.json` +
`\n 2. git commit -m "chore: bump version to ${newVersionString}"` +
`\n 3. git tag v${newVersionString}`
);
} else if (hookMode) {
// En mode hook, ajouter package.json au staging
try {
execSync('git add package.json', { stdio: 'ignore' });
} catch {
// Ignorer les erreurs en mode hook
}
}
} catch (error) {
if (!silent) {
console.error('❌ Erreur lors de la mise à jour de version:', error);
}
if (!hookMode) {
process.exit(1);
}
}
}
main();

Some files were not shown because too many files have changed in this diff Show More