228 Commits

Author SHA1 Message Date
Julien Froidefond
7ce8057c6b chore(dependencies): update Next.js to version 15.5.7, NextAuth to version 4.24.13, and various other dependencies for improved performance and security 2025-12-05 11:50:41 +01:00
Julien Froidefond
5415247f47 chore(dependencies): update eslint-config-next to version 15.5.7 and various TypeScript ESLint packages to version 8.48.1 for improved compatibility and security 2025-12-05 08:23:04 +01:00
Julien Froidefond
f57ea205c7 test(JiraSync): improve test coverage for synchronization scenarios and enhance assertions for change detection 2025-11-26 08:40:48 +01:00
Julien Froidefond
4fc41a5b2c refactor(ManagerWeeklySummary): replace AchievementCard and ChallengeCard with TaskCard, implement tag filtering for accomplishments and challenges, and enhance UI for better data presentation 2025-11-26 08:40:42 +01:00
Julien Froidefond
4c0f227e27 test(JiraSync): further refine test coverage for synchronization and change detection scenarios 2025-11-21 16:47:04 +01:00
Julien Froidefond
ddba4eca37 test(JiraSync): expand test coverage for synchronization and change detection scenarios 2025-11-21 16:41:33 +01:00
Julien Froidefond
411bac8162 test(JiraSync): further enhance test coverage for synchronization and change detection logic 2025-11-21 15:14:54 +01:00
Julien Froidefond
4496cd97f9 test(JiraSync): improve test coverage for synchronization logic and change detection 2025-11-21 14:58:31 +01:00
Julien Froidefond
5c9b2b9d8f test(JiraSync): enhance test coverage for change detection logic and preserved fields 2025-11-21 14:56:16 +01:00
Julien Froidefond
a1d631037e test(JiraSync): add assertion for preserved fields in project change scenario 2025-11-21 14:16:57 +01:00
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
424 changed files with 62070 additions and 24090 deletions

BIN
.DS_Store vendored

Binary file not shown.

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.**

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.

View File

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

View File

@@ -5,6 +5,7 @@ Guide d'utilisation de TowerControl avec Docker.
## 🚀 Démarrage rapide ## 🚀 Démarrage rapide
### Production ### Production
```bash ```bash
# Démarrer le service de production # Démarrer le service de production
docker-compose up -d towercontrol docker-compose up -d towercontrol
@@ -14,6 +15,7 @@ open http://localhost:3006
``` ```
### Développement ### Développement
```bash ```bash
# Démarrer le service de développement avec live reload # Démarrer le service de développement avec live reload
docker-compose --profile dev up towercontrol-dev docker-compose --profile dev up towercontrol-dev
@@ -25,6 +27,7 @@ open http://localhost:3005
## 📋 Services disponibles ## 📋 Services disponibles
### 🚀 `towercontrol` (Production) ### 🚀 `towercontrol` (Production)
- **Port** : 3006 - **Port** : 3006
- **Base de données** : `./data/prod.db` - **Base de données** : `./data/prod.db`
- **Sauvegardes** : `./data/backups/` - **Sauvegardes** : `./data/backups/`
@@ -32,6 +35,7 @@ open http://localhost:3005
- **Restart** : Automatique - **Restart** : Automatique
### 🛠️ `towercontrol-dev` (Développement) ### 🛠️ `towercontrol-dev` (Développement)
- **Port** : 3005 - **Port** : 3005
- **Base de données** : `./data/dev.db` - **Base de données** : `./data/dev.db`
- **Sauvegardes** : `./data/backups/` (partagées) - **Sauvegardes** : `./data/backups/` (partagées)
@@ -55,7 +59,7 @@ open http://localhost:3005
### Variables d'environnement ### Variables d'environnement
| Variable | Production | Développement | Description | | Variable | Production | Développement | Description |
|----------|------------|---------------|-------------| | ---------------------- | --------------------- | -------------------- | ---------------- |
| `NODE_ENV` | `production` | `development` | Mode d'exécution | | `NODE_ENV` | `production` | `development` | Mode d'exécution |
| `DATABASE_URL` | `file:./data/prod.db` | `file:./data/dev.db` | Base Prisma | | `DATABASE_URL` | `file:./data/prod.db` | `file:./data/dev.db` | Base Prisma |
| `BACKUP_DATABASE_PATH` | `./data/prod.db` | `./data/dev.db` | Source backup | | `BACKUP_DATABASE_PATH` | `./data/prod.db` | `./data/dev.db` | Source backup |
@@ -70,6 +74,7 @@ open http://localhost:3005
## 📚 Commandes utiles ## 📚 Commandes utiles
### Gestion des conteneurs ### Gestion des conteneurs
```bash ```bash
# Voir les logs # Voir les logs
docker-compose logs -f towercontrol docker-compose logs -f towercontrol
@@ -86,32 +91,35 @@ docker-compose down -v --rmi all
``` ```
### Gestion des données ### Gestion des données
```bash ```bash
# Sauvegarder les données # Sauvegarder les données
docker-compose exec towercontrol npm run backup:create docker-compose exec towercontrol pnpm run backup:create
# Lister les sauvegardes # Lister les sauvegardes
docker-compose exec towercontrol npm run backup:list docker-compose exec towercontrol pnpm run backup:list
# Accéder au shell du conteneur # Accéder au shell du conteneur
docker-compose exec towercontrol sh docker-compose exec towercontrol sh
``` ```
### Base de données ### Base de données
```bash ```bash
# Migrations Prisma # Migrations Prisma
docker-compose exec towercontrol npx prisma migrate deploy docker-compose exec towercontrol pnpm prisma migrate deploy
# Reset de la base (dev uniquement) # Reset de la base (dev uniquement)
docker-compose exec towercontrol-dev npx prisma migrate reset docker-compose exec towercontrol-dev pnpm prisma migrate reset
# Studio Prisma (dev) # Studio Prisma (dev)
docker-compose exec towercontrol-dev npx prisma studio docker-compose exec towercontrol-dev pnpm prisma studio
``` ```
## 🔍 Debugging ## 🔍 Debugging
### Vérifier la santé ### Vérifier la santé
```bash ```bash
# Health check # Health check
curl http://localhost:3006/api/health curl http://localhost:3006/api/health
@@ -122,6 +130,7 @@ docker-compose exec towercontrol env | grep -E "(DATABASE|BACKUP|NODE_ENV)"
``` ```
### Logs détaillés ### Logs détaillés
```bash ```bash
# Logs avec timestamps # Logs avec timestamps
docker-compose logs -f -t towercontrol docker-compose logs -f -t towercontrol
@@ -135,6 +144,7 @@ docker-compose logs --tail=100 towercontrol
### Problèmes courants ### Problèmes courants
**Port déjà utilisé** **Port déjà utilisé**
```bash ```bash
# Trouver le processus qui utilise le port # Trouver le processus qui utilise le port
lsof -i :3006 lsof -i :3006
@@ -142,12 +152,14 @@ kill -9 <PID>
``` ```
**Base de données corrompue** **Base de données corrompue**
```bash ```bash
# Restaurer depuis une sauvegarde # Restaurer depuis une sauvegarde
docker-compose exec towercontrol npm run backup:restore filename.db.gz docker-compose exec towercontrol pnpm run backup:restore filename.db.gz
``` ```
**Permissions** **Permissions**
```bash ```bash
# Corriger les permissions du dossier data # Corriger les permissions du dossier data
sudo chown -R $USER:$USER ./data sudo chown -R $USER:$USER ./data
@@ -156,6 +168,7 @@ sudo chown -R $USER:$USER ./data
## 📊 Monitoring ## 📊 Monitoring
### Espace disque ### Espace disque
```bash ```bash
# Taille du dossier data # Taille du dossier data
du -sh ./data du -sh ./data
@@ -165,6 +178,7 @@ df -h .
``` ```
### Performance ### Performance
```bash ```bash
# Stats des conteneurs # Stats des conteneurs
docker stats docker stats
@@ -176,6 +190,7 @@ docker-compose exec towercontrol free -h
## 🔒 Production ## 🔒 Production
### Recommandations ### Recommandations
- Utiliser un reverse proxy (nginx, traefik) - Utiliser un reverse proxy (nginx, traefik)
- Configurer HTTPS - Configurer HTTPS
- Sauvegarder régulièrement `./data/` - Sauvegarder régulièrement `./data/`
@@ -183,6 +198,7 @@ docker-compose exec towercontrol free -h
- Logs centralisés - Logs centralisés
### Exemple nginx ### Exemple nginx
```nginx ```nginx
server { server {
listen 80; listen 80;

View File

@@ -1,6 +1,11 @@
# Multi-stage Dockerfile for Next.js with Prisma # Multi-stage Dockerfile for Next.js with Prisma
FROM node:20-alpine AS base 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 # Install dependencies only when needed
FROM base AS deps FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. # 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 WORKDIR /app
# Install dependencies based on the preferred package manager # 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 \ 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; \ else echo "Lockfile not found." && exit 1; \
fi fi
@@ -23,20 +32,17 @@ COPY . .
# Set a dummy DATABASE_URL for build time (Prisma needs it to generate client) # Set a dummy DATABASE_URL for build time (Prisma needs it to generate client)
ENV DATABASE_URL="file:/tmp/build.db" ENV DATABASE_URL="file:/tmp/build.db"
# Generate Prisma client # Generate Prisma client (no DB needed at build time)
RUN npx prisma generate RUN pnpm prisma generate
# Initialize the database schema for build time
RUN npx prisma migrate deploy || npx prisma db push
# Build the application # Build the application
RUN npm run build RUN pnpm run build
# Production image, copy all the files and run next # Production image, copy all the files and run next
FROM base AS runner FROM base AS runner
# Set timezone to Europe/Paris and install sqlite3 for backups # Set timezone to Europe/Paris and install sqlite3 for backups
RUN apk add --no-cache tzdata sqlite RUN apk add --no-cache tzdata sqlite su-exec
RUN ln -snf /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo Europe/Paris > /etc/timezone RUN ln -snf /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo Europe/Paris > /etc/timezone
WORKDIR /app 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/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static 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/prisma ./prisma
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
# Create data directory for SQLite and backups # Copy pnpm node_modules (includes .pnpm store with Prisma client)
RUN mkdir -p /app/data/backups && chown -R nextjs:nodejs /app/data 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 # Set all ENV vars before switching user
ENV PORT=3000 ENV PORT=3000
ENV HOSTNAME="0.0.0.0" ENV HOSTNAME="0.0.0.0"
ENV TZ=Europe/Paris ENV TZ=Europe/Paris
USER nextjs
EXPOSE 3000 EXPOSE 3000
# Start the application with database migration # Start the application with Prisma migrations
CMD ["sh", "-c", "npx prisma migrate deploy && node server.js"] # 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

264
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 ## ✨ Fonctionnalités principales
### 🏗️ Kanban moderne ### 🏗️ Kanban moderne
- **Drag & drop fluide** avec @dnd-kit (optimistic updates) - **Drag & drop fluide** avec @dnd-kit (optimistic updates)
- **Colonnes configurables** : backlog, todo, in_progress, done, cancelled, freeze, archived - **Colonnes configurables** : backlog, todo, in_progress, done, cancelled, freeze, archived
- **Vues multiples** : Kanban classique + swimlanes par priorité - **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 - **Création rapide** : Ajout inline dans chaque colonne
### 🏷️ Système de tags avancé ### 🏷️ Système de tags avancé
- **Tags colorés** avec sélecteur de couleur - **Tags colorés** avec sélecteur de couleur
- **Autocomplete intelligent** lors de la saisie - **Autocomplete intelligent** lors de la saisie
- **Filtrage en temps réel** par tags - **Filtrage en temps réel** par tags
- **Gestion complète** avec page dédiée `/tags` - **Gestion complète** avec page dédiée `/tags`
### 📊 Filtrage et recherche ### 📊 Filtrage et recherche
- **Recherche temps réel** dans les titres et descriptions - **Recherche temps réel** dans les titres et descriptions
- **Filtres combinables** : statut, priorité, tags, source - **Filtres combinables** : statut, priorité, tags, source
- **Tri flexible** : date, priorité, alphabétique - **Tri flexible** : date, priorité, alphabétique
- **Interface intuitive** avec dropdowns et toggles - **Interface intuitive** avec dropdowns et toggles
### 📝 Daily Notes ### 📝 Daily Notes
- **Checkboxes quotidiennes** avec sections "Hier" / "Aujourd'hui" - **Checkboxes quotidiennes** avec sections "Hier" / "Aujourd'hui"
- **Navigation par date** (précédent/suivant) - **Navigation par date** (précédent/suivant)
- **Liaison optionnelle** avec les tâches existantes - **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 - **Historique calendaire** des dailies
### 🔗 Intégration Jira Cloud ### 🔗 Intégration Jira Cloud
- **Synchronisation unidirectionnelle** (Jira → local) - **Synchronisation unidirectionnelle** (Jira → local)
- **Authentification sécurisée** (email + API token) - **Authentification sécurisée** (email + API token)
- **Mapping intelligent** des statuts Jira - **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 de configuration** complète
### 🎨 Interface & UX ### 🎨 Interface & UX
- **Thème adaptatif** : dark/light + détection système - **Thème adaptatif** : dark/light + détection système
- **Design cohérent** : palette cyberpunk/tech avec Tailwind CSS - **Design cohérent** : palette cyberpunk/tech avec Tailwind CSS
- **Composants modulaires** : Button, Input, Card, Modal, Badge - **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 - **Responsive design** pour tous les écrans
### ⚡ Performance & Architecture ### ⚡ Performance & Architecture
- **Server Actions** pour les mutations rapides (vs API routes) - **Server Actions** pour les mutations rapides (vs API routes)
- **Architecture SSR** avec hydratation optimisée - **Architecture SSR** avec hydratation optimisée
- **Base de données SQLite** ultra-rapide - **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 ## 🛠️ Installation
### Prérequis ### Prérequis
- **Node.js** 18+ - **Node.js** 18+
- **npm** ou **yarn** - **pnpm** 9+
### Installation locale ### Installation locale
@@ -83,17 +91,17 @@ git clone https://github.com/votre-repo/towercontrol.git
cd towercontrol cd towercontrol
# Installer les dépendances # Installer les dépendances
npm install pnpm install
# Configurer la base de données # Configurer la base de données
npx prisma generate pnpm prisma generate
npx prisma db push pnpm prisma db push
# (Optionnel) Ajouter des données de test # (Optionnel) Ajouter des données de test
npm run seed pnpm run seed
# Démarrer en développement # Démarrer en développement
npm run dev pnpm run dev
``` ```
L'application sera accessible sur **http://localhost:3000** L'application sera accessible sur **http://localhost:3000**
@@ -115,10 +123,12 @@ docker compose --profile dev up -d
``` ```
**Accès :** **Accès :**
- **Production** : http://localhost:3006 - **Production** : http://localhost:3006
- **Développement** : http://localhost:3005 - **Développement** : http://localhost:3005
**Gestion des données :** **Gestion des données :**
```bash ```bash
# Utiliser votre base locale existante (décommentez dans docker-compose.yml) # Utiliser votre base locale existante (décommentez dans docker-compose.yml)
# - ./prisma/dev.db:/app/data/prod.db # - ./prisma/dev.db:/app/data/prod.db
@@ -134,6 +144,7 @@ docker compose down -v
``` ```
**Avantages Docker :** **Avantages Docker :**
-**Isolation complète** - Pas de pollution de l'environnement local -**Isolation complète** - Pas de pollution de l'environnement local
-**Base persistante** - Volumes Docker pour SQLite -**Base persistante** - Volumes Docker pour SQLite
-**Prêt pour prod** - Configuration optimisée -**Prêt pour prod** - Configuration optimisée
@@ -182,31 +193,204 @@ JIRA_API_TOKEN="votre_token_api"
``` ```
towercontrol/ towercontrol/
├── src/ ├── src/
│ ├── app/ # Pages Next.js 15 (App Router) │ ├── app/ # Next.js 15 App Router (pages & routes)
│ │ ├── api/ # API Routes (endpoints complexes) │ │ ├── api/ # API Routes (endpoints complexes)
│ │ ├── daily/ # Page daily notes │ │ │ ├── analytics/ # Endpoints d'analytics
│ │ ├── tags/ # Page gestion tags │ │ │ ├── auth/ # Authentification (NextAuth)
│ │ └── settings/ # Page configuration │ │ │ ├── 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) │ ├── actions/ # Server Actions (mutations rapides)
└── contexts/ # Contexts React globaux │ ├── backup.ts # Actions sauvegardes
├── components/ │ │ ├── daily.ts # Actions daily notes
│ ├── ui/ # Composants UI de base │ ├── jira-analytics.ts # Actions analytics Jira
│ ├── kanban/ # Composants Kanban │ ├── preferences.ts # Actions préférences
│ ├── daily/ # Composants Daily notes │ ├── tags.ts # Actions tags
└── forms/ # Formulaires réutilisables │ ├── tasks.ts # Actions tâches
├── services/ # Services backend (logique métier) └── tfs.ts # Actions TFS
├── database.ts # Pool Prisma
│ ├── tasks.ts # CRUD tâches │ ├── components/ # Composants React (UI uniquement)
│ ├── tags.ts # CRUD tags │ ├── ui/ # Composants UI de base réutilisables
├── daily.ts # Daily notes ├── Button.tsx # Boutons
├── jira.ts # Intégration Jira ├── Input.tsx # Inputs
└── user-preferences.ts # Préférences utilisateur │ │ ├── Modal.tsx # Modales
├── clients/ # Clients HTTP frontend ├── Badge.tsx # Badges
├── hooks/ # Hooks React personnalisés ├── Card.tsx # Cartes
├── lib/ # Utilitaires et types └── ... # Autres composants UI
└── prisma/ # Schéma et migrations DB ├── 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 ### Stack technique
- **Frontend** : Next.js 15, React 19, TypeScript, Tailwind CSS - **Frontend** : Next.js 15, React 19, TypeScript, Tailwind CSS
@@ -262,22 +446,22 @@ towercontrol/
```bash ```bash
# Développement # Développement
npm run dev # Démarrer en mode dev avec Turbopack pnpm run dev # Démarrer en mode dev avec Turbopack
npm run build # Build de production pnpm run build # Build de production
npm run start # Démarrer en production pnpm run start # Démarrer en production
# Base de données # Base de données
npx prisma studio # Interface graphique BDD pnpm prisma studio # Interface graphique BDD
npx prisma generate # Regénérer le client Prisma pnpm prisma generate # Regénérer le client Prisma
npx prisma db push # Appliquer le schema à la BDD pnpm prisma db push # Appliquer le schema à la BDD
npx prisma migrate dev # Créer une migration pnpm prisma migrate dev # Créer une migration
# Qualité de code # Qualité de code
npm run lint # ESLint + Prettier pnpm run lint # ESLint + Prettier
npx tsc --noEmit # Vérification TypeScript pnpm tsc --noEmit # Vérification TypeScript
# Scripts utilitaires # Scripts utilitaires
npm run seed # Ajouter des données de test pnpm run seed # Ajouter des données de test
``` ```
--- ---
@@ -292,7 +476,7 @@ export const UI_CONFIG = {
theme: 'system', // 'light' | 'dark' | 'system' theme: 'system', // 'light' | 'dark' | 'system'
itemsPerPage: 50, // Pagination itemsPerPage: 50, // Pagination
enableDragAndDrop: true, // Drag & drop enableDragAndDrop: true, // Drag & drop
autoSave: true // Sauvegarde auto autoSave: true, // Sauvegarde auto
}; };
``` ```
@@ -322,6 +506,7 @@ DATABASE_URL="postgresql://user:pass@localhost:5432/towercontrol"
## 🚧 Roadmap ## 🚧 Roadmap
### ✅ Version 2.0 (Actuelle) ### ✅ Version 2.0 (Actuelle)
- Interface Kanban moderne avec drag & drop - Interface Kanban moderne avec drag & drop
- Système de tags avancé - Système de tags avancé
- Daily notes avec navigation - Daily notes avec navigation
@@ -330,12 +515,14 @@ DATABASE_URL="postgresql://user:pass@localhost:5432/towercontrol"
- Server Actions pour les performances - Server Actions pour les performances
### 🔄 Version 2.1 (En cours) ### 🔄 Version 2.1 (En cours)
- [ ] Page dashboard avec analytics - [ ] Page dashboard avec analytics
- [ ] Système de sauvegarde automatique (configurable) - [ ] Système de sauvegarde automatique (configurable)
- [ ] Métriques de productivité et graphiques - [ ] Métriques de productivité et graphiques
- [ ] Actions en lot (sélection multiple) - [ ] Actions en lot (sélection multiple)
### 🎯 Version 2.2 (Futur) ### 🎯 Version 2.2 (Futur)
- [ ] Sous-tâches et hiérarchie - [ ] Sous-tâches et hiérarchie
- [ ] Dates d'échéance et rappels - [ ] Dates d'échéance et rappels
- [ ] Collaboration et assignation - [ ] Collaboration et assignation
@@ -343,6 +530,7 @@ DATABASE_URL="postgresql://user:pass@localhost:5432/towercontrol"
- [ ] Mode PWA et offline - [ ] Mode PWA et offline
### 🚀 Version 3.0 (Vision) ### 🚀 Version 3.0 (Vision)
- [ ] Analytics d'équipe avancées - [ ] Analytics d'équipe avancées
- [ ] Intégrations multiples (GitHub, Linear, etc.) - [ ] Intégrations multiples (GitHub, Linear, etc.)
- [ ] API publique et webhooks - [ ] 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._ 🎯

416
TODO.md
View File

@@ -1,69 +1,64 @@
# TowerControl v2.0 - Gestionnaire de tâches moderne # TowerControl v2.0 - Gestionnaire de tâches moderne
## Autre Todos #2 ## Fix
- [x] Synchro Jira auto en background timé comme pour la synchro de sauvegarde
- [ ] 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é
- [ ] 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 :)
## 🔧 Phase 6: Fonctionnalités avancées (Priorité 6) - [ ] 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
### 6.1 Gestion avancée des tâches ## Idées à developper
- [ ] 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 - [ ] Optimisations Perf : requetes DB
- [ ] 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
- [ ] PWA et mode offline - [ ] PWA et mode offline
--- ---
## 🐛 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 ## 🚀 Nouvelles idées & fonctionnalités futures
### 🔄 Intégration TFS/Azure DevOps
- [ ] **Lecture des Pull Requests TFS** : Synchronisation des PR comme tâches
- [ ] PR arrivent en backlog avec filtrage par team project
- [ ] Synchronisation aussi riche que Jira (statuts, assignés, commentaires)
- [ ] Filtrage par team project, repository, auteur
- [ ] **Architecture plug-and-play pour intégrations**
- [ ] Refactoriser pour interfaces génériques d'intégration
- [ ] Interface `IntegrationService` commune (Jira, TFS, GitHub, etc.)
- [ ] UI générique de configuration des intégrations
- [ ] Système de plugins pour ajouter facilement de nouveaux services
### 📋 Daily - Gestion des tâches non cochées
- [ ] **Page des tâches en attente**
- [ ] Liste de toutes les todos non cochées (historique complet)
- [ ] Filtrage par date, catégorie, ancienneté
- [ ] Action "Archiver" pour les tâches ni résolues ni à faire
- [ ] **Nouveau statut "Archivé"**
- [ ] État intermédiaire entre "à faire" et "terminé"
- [ ] Interface pour voir/gérer les tâches archivées
- [ ] Possibilité de désarchiver une tâche
### 🎯 Jira - Suivi des demandes en attente ### 🎯 Jira - Suivi des demandes en attente
- [ ] **Page "Jiras en attente"** - [ ] **Page "Jiras en attente"**
- [ ] Liste des Jiras créés par moi mais non assignés à mon équipe - [ ] Liste des Jiras créés par moi mais non assignés à mon équipe
- [ ] Suivi des demandes formulées à d'autres équipes - [ ] Suivi des demandes formulées à d'autres équipes
@@ -73,167 +68,218 @@
- [ ] Champs spécifiques : demandeur, équipe cible, statut de traitement - [ ] Champs spécifiques : demandeur, équipe cible, statut de traitement
- [ ] Notifications quand une demande change de statut - [ ] Notifications quand une demande change de statut
### 🏗️ Architecture & technique
- [ ] **Système d'intégrations modulaire**
- [ ] Interface `IntegrationProvider` standardisée
- [ ] Configuration dynamique des intégrations
- [ ] Gestion des credentials par intégration
- [ ] **Modèles de données étendus**
- [ ] `PullRequest` pour TFS/GitHub
- [ ] `PendingRequest` pour les demandes Jira
- [ ] `ArchivedTask` pour les daily archivées
- [ ] **UI générique**
- [ ] Composants réutilisables pour toutes les intégrations
- [ ] Configuration unifiée des filtres et synchronisations
- [ ] Dashboard multi-intégrations
### 📁 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)
```
### 👥 Gestion multi-utilisateurs (PROJET MAJEUR) ### 👥 Gestion multi-utilisateurs (PROJET MAJEUR)
#### **Architecture actuelle → Multi-tenant** #### **Architecture actuelle → Multi-tenant**
- **Problème** : App mono-utilisateur avec données globales - **Problème** : App mono-utilisateur avec données globales
- **Solution** : Transformation en app multi-utilisateurs avec isolation des données - **Solution** : Transformation en app multi-utilisateurs avec isolation des données + système de rôles
#### **Plan de migration** #### **Plan de migration**
- [ ] **Phase 1: Authentification** - [ ] **Phase 1: Authentification**
- [ ] Système de login/mot de passe (NextAuth.js ou custom) - [ ] Système de login/mot de passe (NextAuth.js)
- [ ] Gestion des sessions sécurisées - [ ] Gestion des sessions sécurisées
- [ ] Pages de connexion/inscription/mot de passe oublié - [ ] Pages de connexion/inscription/mot de passe oublié
- [ ] Middleware de protection des routes - [ ] Middleware de protection des routes
- [ ] **Phase 2: Modèle de données multi-tenant** - [ ] **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.) - [ ] Ajouter `userId` à toutes les tables (tasks, daily, tags, preferences, etc.)
- [ ] Migration des données existantes vers un utilisateur par défaut
- [ ] Contraintes de base de données pour l'isolation - [ ] Contraintes de base de données pour l'isolation
- [ ] Index sur `userId` pour les performances - [ ] Index sur `userId` pour les performances
- [ ] **Phase 3: Services et API** - [ ] **Phase 3: Système de rôles et permissions**
- [ ] Modifier tous les services pour filtrer par `userId` - [ ] **Rôle ADMIN**
- [ ] Middleware d'injection automatique du `userId` dans les requêtes - [ ] Gestion complète des utilisateurs (CRUD)
- [ ] Validation que chaque utilisateur ne voit que ses données - [ ] Assignation/modification des rôles
- [ ] API d'administration (optionnel) - [ ] 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: UI et UX** - [ ] **Phase 4: Services et API avec rôles**
- [ ] Header avec profil utilisateur et déconnexion - [ ] **Services utilisateurs**
- [ ] Onboarding pour nouveaux utilisateurs - [ ] `user-management.ts` : CRUD utilisateurs (admin only)
- [ ] Gestion du profil utilisateur - [ ] `team-management.ts` : Gestion des équipes (admin/manager)
- [ ] Partage optionnel entre utilisateurs (équipes) - [ ] `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** #### **Considérations techniques**
- **Base de données** : Ajouter `userId` partout + contraintes - **Base de données** : Ajouter `userId` partout + contraintes
- **Sécurité** : Validation côté serveur de l'isolation des données - **Sécurité** : Validation côté serveur de l'isolation des données
- **Performance** : Index sur `userId`, pagination pour gros volumes - **Performance** : Index sur `userId`, pagination pour gros volumes
- **Migration** : Script de migration des données existantes - **Migration** : Script de migration des données existantes
### 📱 Interface mobile adaptée (PROJET MAJEUR) ---
#### **Problème actuel** ## 🤖 Intégration IA avec Mistral (Phase IA)
- Kanban non adapté aux écrans tactiles petits
- Drag & drop difficile sur mobile
- Interface desktop-first
#### **Solution : Interface mobile dédiée** ### **Socle technique**
- [ ] **Phase 1: Détection et responsive**
- [ ] Détection mobile/desktop (useMediaQuery)
- [ ] Composant de switch automatique d'interface
- [ ] Breakpoints adaptés pour tablettes
- [ ] **Phase 2: Interface mobile pour les tâches** - [ ] **Phase 1: Infrastructure Mistral**
- [ ] **Vue liste simple** : Remplacement du Kanban - [ ] Configuration du client Mistral local
- [ ] Liste verticale avec statuts en badges - [ ] Service `mistral-client.ts` avec connexion au modèle local
- [ ] Actions par swipe (marquer terminé, changer statut) - [ ] Configuration des endpoints et paramètres (température, tokens, etc.)
- [ ] Filtres simplifiés (dropdown au lieu de sidebar) - [ ] Gestion des erreurs et timeouts
- [ ] **Actions tactiles** - [ ] Cache des réponses pour éviter les appels répétés
- [ ] Tap pour voir détails - [ ] **Système de prompts**
- [ ] Long press pour menu contextuel - [ ] Template engine pour les prompts structurés
- [ ] Swipe left/right pour actions rapides - [ ] Prompts spécialisés par fonctionnalité (analyse, génération, classification)
- [ ] **Navigation mobile** - [ ] Versioning des prompts pour A/B testing
- [ ] Bottom navigation bar - [ ] Logging des interactions pour amélioration continue
- [ ] Sections : Tâches, Daily, Jira, Profil - [ ] **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 3: Daily mobile optimisé** - [ ] **Phase 2: Services IA développés avec les features**
- [ ] Checkboxes plus grandes (touch-friendly) - [ ] Services créés au fur et à mesure des besoins des fonctionnalités
- [ ] Ajout rapide par bouton flottant - [ ] Pas de développement anticipé - implémentation juste-à-temps
- [ ] Calendrier mobile avec navigation par swipe - [ ] Architecture modulaire pour faciliter l'ajout de nouveaux services
- [ ] **Phase 4: Jira mobile** - [ ] **Phase 3: Configuration et gestion de l'assistant**
- [ ] Métriques simplifiées (cartes au lieu de graphiques complexes) - [ ] **Page de configuration IA (/settings/ai-assistant)**
- [ ] Filtres en modal/drawer - [ ] Configuration du modèle Mistral (endpoint, température, max tokens)
- [ ] Synchronisation en background - [ ] 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
#### **Composants mobiles spécifiques** ### **Fonctionnalités IA concrètes**
```typescript
// Exemples de composants à créer
- MobileTaskList.tsx // Remplace le Kanban
- MobileTaskCard.tsx // Version tactile des cartes
- MobileNavigation.tsx // Bottom nav
- SwipeActions.tsx // Actions par swipe
- MobileDailyView.tsx // Daily optimisé mobile
- MobileFilters.tsx // Filtres en modal
```
#### **Considérations UX mobile** #### 🎯 **Smart Task Creation**
- **Simplicité** : Moins d'options visibles, plus de navigation
- **Tactile** : Boutons plus grands, zones de touch optimisées - [ ] **Bouton "Créer avec IA" dans le Kanban**
- **Performance** : Lazy loading, virtualisation pour longues listes - [ ] Input libre : "Préparer présentation client pour vendredi"
- **Offline** : Cache local pour usage sans réseau (PWA) - [ ] 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.* _Focus sur l'expérience utilisateur et le design moderne. App standalone prête pour évoluer vers une plateforme d'intégration complète._

View File

@@ -3,6 +3,7 @@
## ✅ Phase 1: Nettoyage et architecture (TERMINÉ) ## ✅ Phase 1: Nettoyage et architecture (TERMINÉ)
### 1.1 Configuration projet Next.js ### 1.1 Configuration projet Next.js
- [x] Initialiser Next.js avec TypeScript - [x] Initialiser Next.js avec TypeScript
- [x] Configurer ESLint, Prettier - [x] Configurer ESLint, Prettier
- [x] Setup structure de dossiers selon les règles du workspace - [x] Setup structure de dossiers selon les règles du workspace
@@ -10,12 +11,14 @@
- [x] Setup Prisma ORM - [x] Setup Prisma ORM
### 1.2 Architecture backend standalone ### 1.2 Architecture backend standalone
- [x] Créer `services/database.ts` - Pool de connexion DB - [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 `services/tasks.ts` - Service CRUD pour les tâches
- [x] Créer `lib/types.ts` - Types partagés (Task, Tag, etc.) - [x] Créer `lib/types.ts` - Types partagés (Task, Tag, etc.)
- [x] Nettoyer l'ancien code de synchronisation - [x] Nettoyer l'ancien code de synchronisation
### 1.3 API moderne et propre ### 1.3 API moderne et propre
- [x] `app/api/tasks/route.ts` - API CRUD complète (GET, POST, PATCH, DELETE) - [x] `app/api/tasks/route.ts` - API CRUD complète (GET, POST, PATCH, DELETE)
- [x] Supprimer les routes de synchronisation obsolètes - [x] Supprimer les routes de synchronisation obsolètes
- [x] Configuration moderne dans `lib/config.ts` - [x] Configuration moderne dans `lib/config.ts`
@@ -25,12 +28,14 @@
## 🎯 Phase 2: Interface utilisateur moderne (EN COURS) ## 🎯 Phase 2: Interface utilisateur moderne (EN COURS)
### 2.1 Système de design et composants UI ### 2.1 Système de design et composants UI
- [x] Créer les composants UI de base (Button, Input, Card, Modal, Badge) - [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] Implémenter le système de design tech dark (couleurs, typographie, spacing)
- [x] Setup Tailwind CSS avec classes utilitaires personnalisées - [x] Setup Tailwind CSS avec classes utilitaires personnalisées
- [x] Créer une palette de couleurs tech/cyberpunk - [x] Créer une palette de couleurs tech/cyberpunk
### 2.2 Composants Kanban existants (à améliorer) ### 2.2 Composants Kanban existants (à améliorer)
- [x] `components/kanban/Board.tsx` - Tableau Kanban principal - [x] `components/kanban/Board.tsx` - Tableau Kanban principal
- [x] `components/kanban/Column.tsx` - Colonnes du Kanban - [x] `components/kanban/Column.tsx` - Colonnes du Kanban
- [x] `components/kanban/TaskCard.tsx` - Cartes de tâches - [x] `components/kanban/TaskCard.tsx` - Cartes de tâches
@@ -38,6 +43,7 @@
- [x] Refactoriser les composants pour utiliser le nouveau système UI - [x] Refactoriser les composants pour utiliser le nouveau système UI
### 2.3 Gestion des tâches (CRUD) ### 2.3 Gestion des tâches (CRUD)
- [x] Formulaire de création de tâche (Modal + Form) - [x] Formulaire de création de tâche (Modal + Form)
- [x] Création rapide inline dans les colonnes (QuickAddTask) - [x] Création rapide inline dans les colonnes (QuickAddTask)
- [x] Formulaire d'édition de tâche (Modal + Form avec pré-remplissage) - [x] Formulaire d'édition de tâche (Modal + Form avec pré-remplissage)
@@ -47,6 +53,7 @@
- [x] Validation des formulaires et gestion d'erreurs - [x] Validation des formulaires et gestion d'erreurs
### 2.4 Gestion des tags ### 2.4 Gestion des tags
- [x] Créer/éditer des tags avec sélecteur de couleur - [x] Créer/éditer des tags avec sélecteur de couleur
- [x] Autocomplete pour les tags existants - [x] Autocomplete pour les tags existants
- [x] Suppression de tags (avec vérification des dépendances) - [x] Suppression de tags (avec vérification des dépendances)
@@ -66,6 +73,7 @@
- [x] Intégration des filtres dans KanbanBoard - [x] Intégration des filtres dans KanbanBoard
### 2.5 Clients HTTP et hooks ### 2.5 Clients HTTP et hooks
- [x] `clients/tasks-client.ts` - Client pour les tâches (CRUD complet) - [x] `clients/tasks-client.ts` - Client pour les tâches (CRUD complet)
- [x] `clients/tags-client.ts` - Client pour les tags - [x] `clients/tags-client.ts` - Client pour les tags
- [x] `clients/base/http-client.ts` - Client HTTP de base - [x] `clients/base/http-client.ts` - Client HTTP de base
@@ -76,6 +84,7 @@
- [x] Architecture SSR + hydratation client optimisée - [x] Architecture SSR + hydratation client optimisée
### 2.6 Fonctionnalités Kanban avancées ### 2.6 Fonctionnalités Kanban avancées
- [x] Drag & drop entre colonnes (@dnd-kit avec React 19) - [x] Drag & drop entre colonnes (@dnd-kit avec React 19)
- [x] Drag & drop optimiste (mise à jour immédiate + rollback si erreur) - [x] Drag & drop optimiste (mise à jour immédiate + rollback si erreur)
- [x] Filtrage par statut/priorité/assigné - [x] Filtrage par statut/priorité/assigné
@@ -85,6 +94,7 @@
- [x] Tri des tâches (date, priorité, alphabétique) - [x] Tri des tâches (date, priorité, alphabétique)
### 2.7 Système de thèmes (clair/sombre) ### 2.7 Système de thèmes (clair/sombre)
- [x] Créer le contexte de thème (ThemeContext + ThemeProvider) - [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] 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] Définir les variables CSS pour le thème clair
@@ -99,6 +109,7 @@
## 📊 Phase 3: Intégrations et analytics (Priorité 3) ## 📊 Phase 3: Intégrations et analytics (Priorité 3)
### 3.1 Gestion du Daily ### 3.1 Gestion du Daily
- [x] Créer `services/daily.ts` - Service de gestion des daily notes - [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] Modèle de données Daily (date, checkboxes hier/aujourd'hui)
- [x] Interface Daily avec sections "Hier" et "Aujourd'hui" - [x] Interface Daily avec sections "Hier" et "Aujourd'hui"
@@ -111,6 +122,7 @@
- [x] Vue calendar/historique des dailies - [x] Vue calendar/historique des dailies
### 3.2 Intégration Jira Cloud ### 3.2 Intégration Jira Cloud
- [x] Créer `services/jira.ts` - Service de connexion à l'API 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] Configuration Jira (URL, email, API token) dans `lib/config.ts`
- [x] Authentification Basic Auth (email + API token) - [x] Authentification Basic Auth (email + API token)
@@ -127,6 +139,7 @@
- [x] Gestion des erreurs et timeouts API - [x] Gestion des erreurs et timeouts API
### 3.3 Page d'accueil/dashboard ### 3.3 Page d'accueil/dashboard
- [x] Créer une page d'accueil moderne avec vue d'ensemble - [x] Créer une page d'accueil moderne avec vue d'ensemble
- [x] Widgets de statistiques (tâches par statut, priorité, etc.) - [x] Widgets de statistiques (tâches par statut, priorité, etc.)
- [x] Déplacer kanban vers /kanban et créer nouveau dashboard à la racine - [x] Déplacer kanban vers /kanban et créer nouveau dashboard à la racine
@@ -137,6 +150,7 @@
- [x] Intégration des analytics dans le dashboard - [x] Intégration des analytics dans le dashboard
### 3.4 Analytics et métriques ### 3.4 Analytics et métriques
- [x] `services/analytics.ts` - Calculs statistiques - [x] `services/analytics.ts` - Calculs statistiques
- [x] Métriques de productivité (vélocité, temps moyen, etc.) - [x] Métriques de productivité (vélocité, temps moyen, etc.)
- [x] Graphiques avec Recharts (tendances, vélocité, distribution) - [x] Graphiques avec Recharts (tendances, vélocité, distribution)
@@ -144,6 +158,7 @@
- [x] Insights automatiques et métriques visuelles - [x] Insights automatiques et métriques visuelles
## Autre Todo ## Autre Todo
- [x] Avoir un bouton pour réduire/agrandir la font des taches dans les kanban (swimlane et classique) - [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] 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] Settings synchro Jira : ajouter une liste de projet à ignorer, doit etre pris en compte par le service bien sur
@@ -161,13 +176,14 @@
- [x] Vérification d'intégrité et restauration sécurisée - [x] Vérification d'intégrité et restauration sécurisée
- [x] Option de restauration depuis une sauvegarde sélectionnée - [x] Option de restauration depuis une sauvegarde sélectionnée
## 🔧 Phase 4: Server Actions - Migration API Routes (Nouveau) ## 🔧 Phase 4: Server Actions - Migration API Routes (Nouveau)
### 4.1 Migration vers Server Actions - Actions rapides ### 4.1 Migration vers Server Actions - Actions rapides
**Objectif** : Remplacer les API routes par des server actions pour les actions simples et fréquentes **Objectif** : Remplacer les API routes par des server actions pour les actions simples et fréquentes
#### Actions TaskCard (Priorité 1) #### Actions TaskCard (Priorité 1)
- [x] Créer `actions/tasks.ts` avec server actions de base - [x] Créer `actions/tasks.ts` avec server actions de base
- [x] `updateTaskStatus(taskId, status)` - Changement de statut - [x] `updateTaskStatus(taskId, status)` - Changement de statut
- [x] `updateTaskTitle(taskId, title)` - Édition inline du titre - [x] `updateTaskTitle(taskId, title)` - Édition inline du titre
@@ -181,6 +197,7 @@
- [x] **Nettoyage** : Modifier `useTasks.ts` pour remplacer mutations par server actions - [x] **Nettoyage** : Modifier `useTasks.ts` pour remplacer mutations par server actions
#### Actions Daily (Priorité 2) #### Actions Daily (Priorité 2)
- [x] Créer `actions/daily.ts` pour les checkboxes - [x] Créer `actions/daily.ts` pour les checkboxes
- [x] `toggleCheckbox(checkboxId)` - Toggle état checkbox - [x] `toggleCheckbox(checkboxId)` - Toggle état checkbox
- [x] `addCheckboxToDaily(dailyId, content)` - Ajouter checkbox - [x] `addCheckboxToDaily(dailyId, content)` - Ajouter checkbox
@@ -193,6 +210,7 @@
- [x] **Nettoyage** : Modifier hook `useDaily.ts` pour `useTransition` - [x] **Nettoyage** : Modifier hook `useDaily.ts` pour `useTransition`
#### Actions User Preferences (Priorité 3) #### Actions User Preferences (Priorité 3)
- [x] Créer `actions/preferences.ts` pour les toggles - [x] Créer `actions/preferences.ts` pour les toggles
- [x] `updateViewPreferences(preferences)` - Préférences d'affichage - [x] `updateViewPreferences(preferences)` - Préférences d'affichage
- [x] `updateKanbanFilters(filters)` - Filtres Kanban - [x] `updateKanbanFilters(filters)` - Filtres Kanban
@@ -204,6 +222,7 @@
- [x] **Nettoyage** : Modifier `UserPreferencesContext.tsx` pour server actions - [x] **Nettoyage** : Modifier `UserPreferencesContext.tsx` pour server actions
#### Actions Tags (Priorité 4) #### Actions Tags (Priorité 4)
- [x] Créer `actions/tags.ts` pour la gestion tags - [x] Créer `actions/tags.ts` pour la gestion tags
- [x] `createTag(name, color)` - Création tag - [x] `createTag(name, color)` - Création tag
- [x] `updateTag(tagId, data)` - Modification tag - [x] `updateTag(tagId, data)` - Modification tag
@@ -214,29 +233,35 @@
- [x] **Nettoyage** : Modifier `useTags.ts` pour server actions directes - [x] **Nettoyage** : Modifier `useTags.ts` pour server actions directes
#### Migration progressive avec nettoyage immédiat #### Migration progressive avec nettoyage immédiat
**Principe** : Pour chaque action migrée → nettoyage immédiat des routes et code obsolètes **Principe** : Pour chaque action migrée → nettoyage immédiat des routes et code obsolètes
### 4.2 Conservation API Routes - Endpoints complexes ### 4.2 Conservation API Routes - Endpoints complexes
**À GARDER en API routes** (pas de migration) **À GARDER en API routes** (pas de migration)
#### Endpoints de fetching initial #### Endpoints de fetching initial
-`GET /api/tasks` - Récupération avec filtres complexes -`GET /api/tasks` - Récupération avec filtres complexes
-`GET /api/daily` - Vue daily avec logique métier -`GET /api/daily` - Vue daily avec logique métier
-`GET /api/tags` - Liste tags avec recherche -`GET /api/tags` - Liste tags avec recherche
-`GET /api/user-preferences` - Préférences initiales -`GET /api/user-preferences` - Préférences initiales
#### Endpoints d'intégration externe #### Endpoints d'intégration externe
-`POST /api/jira/sync` - Synchronisation Jira complexe -`POST /api/jira/sync` - Synchronisation Jira complexe
-`GET /api/jira/logs` - Logs de synchronisation -`GET /api/jira/logs` - Logs de synchronisation
- ✅ Configuration Jira (formulaires complexes) - ✅ Configuration Jira (formulaires complexes)
#### Raisons de conservation #### Raisons de conservation
- **API publique** : Réutilisable depuis mobile/externe - **API publique** : Réutilisable depuis mobile/externe
- **Logique complexe** : Synchronisation, analytics, rapports - **Logique complexe** : Synchronisation, analytics, rapports
- **Monitoring** : Besoin de logs HTTP séparés - **Monitoring** : Besoin de logs HTTP séparés
- **Real-time futur** : WebSockets/SSE non compatibles server actions - **Real-time futur** : WebSockets/SSE non compatibles server actions
### 4.3 Architecture hybride cible ### 4.3 Architecture hybride cible
``` ```
Actions rapides → Server Actions directes Actions rapides → Server Actions directes
├── TaskCard actions (status, title, delete) ├── TaskCard actions (status, title, delete)
@@ -252,6 +277,7 @@ Endpoints complexes → API Routes conservées
``` ```
### 4.4 Avantages attendus ### 4.4 Avantages attendus
- **🚀 Performance** : Pas de sérialisation HTTP pour actions rapides - **🚀 Performance** : Pas de sérialisation HTTP pour actions rapides
- **🔄 Cache intelligent** : `revalidatePath()` automatique - **🔄 Cache intelligent** : `revalidatePath()` automatique
- **📦 Bundle reduction** : Moins de code client HTTP - **📦 Bundle reduction** : Moins de code client HTTP
@@ -261,6 +287,7 @@ Endpoints complexes → API Routes conservées
## 📊 Phase 5: Surveillance Jira - Analytics d'équipe (Priorité 5) ## 📊 Phase 5: Surveillance Jira - Analytics d'équipe (Priorité 5)
### 5.1 Configuration projet Jira ### 5.1 Configuration projet Jira
- [x] Ajouter champ `projectKey` dans la config Jira (settings) - [x] Ajouter champ `projectKey` dans la config Jira (settings)
- [x] Interface pour sélectionner le projet à surveiller - [x] Interface pour sélectionner le projet à surveiller
- [x] Validation de l'existence du projet via API Jira - [x] Validation de l'existence du projet via API Jira
@@ -268,6 +295,7 @@ Endpoints complexes → API Routes conservées
- [x] Test de connexion spécifique au projet configuré - [x] Test de connexion spécifique au projet configuré
### 5.2 Service d'analytics Jira ### 5.2 Service d'analytics Jira
- [x] Créer `services/jira-analytics.ts` - Métriques avancées - [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] 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] Calculs de vélocité d'équipe (story points par sprint)
@@ -278,6 +306,7 @@ Endpoints complexes → API Routes conservées
- [x] Cache intelligent des métriques (éviter API rate limits) - [x] Cache intelligent des métriques (éviter API rate limits)
### 5.3 Page de surveillance `/jira-dashboard` ### 5.3 Page de surveillance `/jira-dashboard`
- [x] Créer page dédiée avec navigation depuis settings Jira - [x] Créer page dédiée avec navigation depuis settings Jira
- [x] Vue d'ensemble du projet (nom, lead, statut global) - [x] Vue d'ensemble du projet (nom, lead, statut global)
- [x] Sélecteur de période (7j, 30j, 3 mois, sprint actuel) - [x] Sélecteur de période (7j, 30j, 3 mois, sprint actuel)
@@ -287,6 +316,7 @@ Endpoints complexes → API Routes conservées
- [x] Alertes visuelles (tickets en retard, sprints déviants) - [x] Alertes visuelles (tickets en retard, sprints déviants)
### 5.4 Métriques et graphiques avancés ### 5.4 Métriques et graphiques avancés
- [x] **Vélocité** : Story points complétés par sprint - [x] **Vélocité** : Story points complétés par sprint
- [x] **Burndown chart** : Progression vs planifié - [x] **Burndown chart** : Progression vs planifié
- [x] **Cycle time** : Temps moyen par type de ticket - [x] **Cycle time** : Temps moyen par type de ticket
@@ -297,6 +327,7 @@ Endpoints complexes → API Routes conservées
- [x] **Collaboration** : Matrice d'interactions entre assignees - [x] **Collaboration** : Matrice d'interactions entre assignees
### 5.5 Fonctionnalités de surveillance ### 5.5 Fonctionnalités de surveillance
- [x] **Cache serveur intelligent** : Cache en mémoire avec invalidation manuelle - [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] **Export des métriques** : Export CSV/JSON avec téléchargement automatique
- [x] **Comparaison inter-sprints** : Tendances, prédictions et recommandations - [x] **Comparaison inter-sprints** : Tendances, prédictions et recommandations
@@ -304,3 +335,261 @@ Endpoints complexes → API Routes conservées
- [x] Filtrage par composant, version, type de ticket - [x] Filtrage par composant, version, type de ticket
- [x] Vue détaillée par sprint avec drill-down - [x] Vue détaillée par sprint avec drill-down
- [x] ~~Intégration avec les daily notes (mentions des blockers)~~ (supprimé) - [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

12
data/README.md Normal file → Executable file
View File

@@ -18,11 +18,13 @@ data/
## 🎯 Utilisation ## 🎯 Utilisation
### En développement local ### En développement local
- La base de données principale est dans `prisma/dev.db` - La base de données principale est dans `prisma/dev.db`
- Ce dossier `data/` est utilisé uniquement par Docker - Ce dossier `data/` est utilisé uniquement par Docker
- Les sauvegardes locales sont dans `backups/` (racine du projet) - Les sauvegardes locales sont dans `backups/` (racine du projet)
### En production Docker ### En production Docker
- Base de données : `data/prod.db` ou `data/dev.db` - Base de données : `data/prod.db` ou `data/dev.db`
- Sauvegardes : `data/backups/` - Sauvegardes : `data/backups/`
- Tout ce dossier est mappé vers `/app/data` dans le conteneur - Tout ce dossier est mappé vers `/app/data` dans le conteneur
@@ -45,12 +47,14 @@ BACKUP_STORAGE_PATH="./data/backups"
## 🗂️ Fichiers ## 🗂️ Fichiers
### Bases de données SQLite ### Bases de données SQLite
- **prod.db** : Base de données de production - **prod.db** : Base de données de production
- **dev.db** : Base de données de développement Docker - **dev.db** : Base de données de développement Docker
- Format : SQLite 3 - Format : SQLite 3
- Contient : Tasks, Tags, User Preferences, Sync Logs, etc. - Contient : Tasks, Tags, User Preferences, Sync Logs, etc.
### Sauvegardes ### Sauvegardes
- **Format** : `towercontrol_YYYY-MM-DDTHH-mm-ss-sssZ.db.gz` - **Format** : `towercontrol_YYYY-MM-DDTHH-mm-ss-sssZ.db.gz`
- **Compression** : gzip - **Compression** : gzip
- **Rétention** : Configurable (défaut: 5 sauvegardes) - **Rétention** : Configurable (défaut: 5 sauvegardes)
@@ -60,16 +64,16 @@ BACKUP_STORAGE_PATH="./data/backups"
```bash ```bash
# Créer une sauvegarde manuelle # Créer une sauvegarde manuelle
npm run backup:create pnpm run backup:create
# Lister les sauvegardes # Lister les sauvegardes
npm run backup:list pnpm run backup:list
# Voir la configuration # Voir la configuration
npm run backup:config pnpm run backup:config
# Restaurer une sauvegarde (dev uniquement) # Restaurer une sauvegarde (dev uniquement)
npm run backup:restore filename.db.gz pnpm run backup:restore filename.db.gz
``` ```
## ⚠️ Important ## ⚠️ Important

View File

@@ -5,18 +5,27 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
target: runner target: runner
ports: ports:
- "3006:3000" - '${PORT:-3007}:3000'
environment: environment:
NODE_ENV: production NODE_ENV: ${NODE_ENV:-production}
DATABASE_URL: "file:../data/dev.db" # Prisma DATABASE_URL: ${DATABASE_URL:-file:/app/data/dev.db}
BACKUP_DATABASE_PATH: "./data/dev.db" # Base de données à sauvegarder BACKUP_DATABASE_PATH: ${BACKUP_DATABASE_PATH:-./data/dev.db}
BACKUP_STORAGE_PATH: "./data/backups" # Dossier des sauvegardes BACKUP_STORAGE_PATH: ${BACKUP_STORAGE_PATH:-./data/backups}
TZ: Europe/Paris 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: volumes:
- ./data:/app/data # Dossier local data/ vers /app/data - ./data:/app/data # Dossier local data/ vers /app/data
restart: unless-stopped restart: unless-stopped
healthcheck: healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/health"] test: ['CMD', 'wget', '-qO-', 'http://localhost:3000/api/health']
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
@@ -28,31 +37,40 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
target: base target: base
ports: ports:
- "3005:3000" - '${PORT_DEV:-3005}:3000'
environment: environment:
NODE_ENV: development NODE_ENV: ${NODE_ENV:-development}
DATABASE_URL: "file:../data/dev.db" # Prisma DATABASE_URL: ${DATABASE_URL:-file:/app/data/dev.db}
BACKUP_DATABASE_PATH: "./data/dev.db" # Base de données à sauvegarder BACKUP_DATABASE_PATH: ${BACKUP_DATABASE_PATH:-./data/dev.db}
BACKUP_STORAGE_PATH: "./data/backups" # Dossier des sauvegardes BACKUP_STORAGE_PATH: ${BACKUP_STORAGE_PATH:-./data/backups}
TZ: Europe/Paris 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: volumes:
- .:/app # code en live - .:/app # code en live
- /app/node_modules # vol anonyme pour ne pas écraser ceux du conteneur - /app/node_modules # vol anonyme pour ne pas écraser ceux du conteneur
- /app/.next - /app/.next
- ./data:/app/data # Dossier local data/ vers /app/data - ./data:/app/data # Dossier local data/ vers /app/data
command: > command: >
sh -c "npm install && sh -c "pnpm install &&
npx prisma generate && pnpm prisma generate &&
npx prisma migrate deploy && (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)) &&
npm run dev" pnpm run dev"
profiles: profiles:
- dev - dev
# 📁 Structure des données : # 📁 Structure des données :
# ./data/ -> /app/data (bind mount) # ./data/ -> /app/data (bind mount)
# ├── prod.db -> Base de données production # ├── prod.db -> Base de données production
# ├── dev.db -> Base de données développement # ├── dev.db -> Base de données développement
# └── backups/ -> Sauvegardes automatiques # └── backups/ -> Sauvegardes automatiques
# #
# 🔧 Configuration via .env.docker # 🔧 Configuration via variables d'environnement (.env ou .env.local)
# 📚 Documentation : ./data/README.md # Les variables utilisent la syntaxe ${VAR:-default} pour les fallbacks
# 📚 Documentation : ./data/README.md et env.example

View File

@@ -14,6 +14,10 @@ JIRA_BASE_URL="" # https://votre-domaine.atlassian.net
JIRA_EMAIL="" # votre.email@domaine.com JIRA_EMAIL="" # votre.email@domaine.com
JIRA_API_TOKEN="" # Token API Jira 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) # Debug (optionnel)
VERBOSE_LOGGING="false" # Logs détaillés en développement VERBOSE_LOGGING="false" # Logs détaillés en développement
NODE_ENV="development" # development | production NODE_ENV="development" # development | production

View File

@@ -1,6 +1,6 @@
import { dirname } from "path"; import { dirname } from 'path';
import { fileURLToPath } from "url"; import { fileURLToPath } from 'url';
import { FlatCompat } from "@eslint/eslintrc"; import { FlatCompat } from '@eslint/eslintrc';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
@@ -10,14 +10,16 @@ const compat = new FlatCompat({
}); });
const eslintConfig = [ const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"), ...compat.extends('next/core-web-vitals', 'next/typescript'),
{ {
ignores: [ ignores: [
"node_modules/**", 'node_modules/**',
".next/**", '.next/**',
"out/**", 'out/**',
"build/**", 'build/**',
"next-env.d.ts", '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,11 +1,52 @@
import type { NextConfig } from "next"; import type { NextConfig } from 'next';
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
output: 'standalone', 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: { turbopack: {
root: process.cwd(),
rules: { rules: {
'*.sql': ['raw'], '*.sql': ['raw'],
} },
}, },
}; };

9209
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,50 +1,92 @@
{ {
"name": "towercontrol", "name": "towercontrol",
"version": "0.1.0", "version": "1.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev --turbopack",
"build": "next build --turbopack", "build": "prisma generate && next build --turbopack",
"start": "next start", "start": "next start",
"postinstall": "prisma generate",
"lint": "eslint", "lint": "eslint",
"backup:create": "npx tsx scripts/backup-manager.ts create", "backup:create": "pnpm tsx scripts/backup-manager.ts create",
"backup:list": "npx tsx scripts/backup-manager.ts list", "backup:list": "pnpm tsx scripts/backup-manager.ts list",
"backup:verify": "npx tsx scripts/backup-manager.ts verify", "backup:verify": "pnpm tsx scripts/backup-manager.ts verify",
"backup:config": "npx tsx scripts/backup-manager.ts config", "backup:config": "pnpm tsx scripts/backup-manager.ts config",
"backup:start": "npx tsx scripts/backup-manager.ts scheduler-start", "backup:start": "pnpm tsx scripts/backup-manager.ts scheduler-start",
"backup:stop": "npx tsx scripts/backup-manager.ts scheduler-stop", "backup:stop": "pnpm tsx scripts/backup-manager.ts scheduler-stop",
"backup:status": "npx tsx scripts/backup-manager.ts scheduler-status" "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": { "dependencies": {
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@emoji-mart/data": "^1.2.1",
"@emoji-mart/react": "^1.1.1",
"@prisma/client": "^6.16.1", "@prisma/client": "^6.16.1",
"@types/jspdf": "^1.3.3", "bcryptjs": "^3.0.2",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"jspdf": "^3.0.3", "emoji-mart": "^5.6.0",
"next": "15.5.3", "emoji-regex": "^10.5.0",
"lucide-react": "^0.544.0",
"mermaid": "^11.12.0",
"next": "15.5.7",
"next-auth": "^4.24.12",
"prism-react-renderer": "^2.4.1",
"prisma": "^6.16.1", "prisma": "^6.16.1",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-markdown": "^10.1.0",
"recharts": "^3.2.1", "recharts": "^3.2.1",
"sqlite3": "^5.1.7", "rehype-raw": "^7.0.0",
"tailwind-merge": "^3.3.1" "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": { "devDependencies": {
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3.3.3",
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4.1.17",
"@types/bcryptjs": "^2.4.6",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.5.3", "eslint-config-next": "^15.5.7",
"eslint-config-prettier": "^10.1.8", "husky": "^9.1.7",
"eslint-plugin-prettier": "^5.5.4", "knip": "^5.71.0",
"lint-staged": "^15.5.2",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"tailwindcss": "^4", "sharp": "^0.34.5",
"tailwindcss": "^4.1.17",
"tsx": "^4.19.2", "tsx": "^4.19.2",
"typescript": "^5" "typescript": "^5",
"vitest": "^2.1.8"
},
"pnpm": {
"overrides": {
"esbuild": ">=0.25.0",
"mdast-util-to-hast": ">=13.2.1"
}
},
"lint-staged": {
"*.{js,jsx,ts,tsx,json,css,md}": [
"prettier --write"
]
} }
} }

7959
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
const config = { const config = {
plugins: ["@tailwindcss/postcss"], plugins: ['@tailwindcss/postcss'],
}; };
export default config; 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 { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
} }
@@ -10,28 +7,56 @@ datasource db {
url = env("DATABASE_URL") url = env("DATABASE_URL")
} }
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 { model Task {
id String @id @default(cuid()) id String @id @default(cuid())
title String title String
description String? description String?
status String @default("todo") status String @default("todo")
priority String @default("medium") priority String @default("medium")
source String // "reminders" | "jira" source String
sourceId String? // ID dans le système source sourceId String?
dueDate DateTime? dueDate DateTime?
completedAt DateTime? completedAt DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
// Métadonnées Jira
jiraProject String? jiraProject String?
jiraKey String? jiraKey String?
jiraType String? // Type de ticket Jira: Story, Task, Bug, Epic, etc. assignee String? // Legacy field - keep for Jira/TFS compatibility
assignee String? ownerId String // Required - chaque tâche appartient à un user
owner User @relation("TaskOwner", fields: [ownerId], references: [id], onDelete: Cascade)
// Relations jiraType String?
taskTags TaskTag[] tfsProject String?
tfsPullRequestId Int?
tfsRepository String?
tfsSourceBranch String?
tfsTargetBranch String?
primaryTagId String?
primaryTag Tag? @relation("PrimaryTag", fields: [primaryTagId], references: [id])
dailyCheckboxes DailyCheckbox[] dailyCheckboxes DailyCheckbox[]
taskTags TaskTag[]
notes Note[] // Notes associées à cette tâche
@@unique([source, sourceId]) @@unique([source, sourceId])
@@map("tasks") @@map("tasks")
@@ -39,19 +64,24 @@ model Task {
model Tag { model Tag {
id String @id @default(cuid()) id String @id @default(cuid())
name String @unique name String
color String @default("#6b7280") color String @default("#6b7280")
isPinned Boolean @default(false) // Tag pour objectifs principaux isPinned Boolean @default(false)
ownerId String // Chaque tag appartient à un utilisateur
owner User @relation("TagOwner", fields: [ownerId], references: [id], onDelete: Cascade)
taskTags TaskTag[] taskTags TaskTag[]
primaryTasks Task[] @relation("PrimaryTag")
noteTags NoteTag[]
@@unique([name, ownerId]) // Un nom de tag unique par utilisateur
@@map("tags") @@map("tags")
} }
model TaskTag { model TaskTag {
taskId String taskId String
tagId String tagId String
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], 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]) @@id([taskId, tagId])
@@map("task_tags") @@map("task_tags")
@@ -59,8 +89,8 @@ model TaskTag {
model SyncLog { model SyncLog {
id String @id @default(cuid()) id String @id @default(cuid())
source String // "reminders" | "jira" source String
status String // "success" | "error" status String
message String? message String?
tasksSync Int @default(0) tasksSync Int @default(0)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@ -70,43 +100,61 @@ model SyncLog {
model DailyCheckbox { model DailyCheckbox {
id String @id @default(cuid()) id String @id @default(cuid())
date DateTime // Date de la checkbox (YYYY-MM-DD) date DateTime
text String // Texte de la checkbox text String
isChecked Boolean @default(false) isChecked Boolean @default(false)
type String @default("task") // "task" | "meeting" type String @default("task")
order Int @default(0) // Ordre d'affichage pour cette date order Int @default(0)
taskId String? // Liaison optionnelle vers une tâche taskId String?
userId String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
task Task? @relation(fields: [taskId], references: [id])
// Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade)
task Task? @relation(fields: [taskId], references: [id], onDelete: SetNull)
@@index([date]) @@index([date])
@@index([userId])
@@map("daily_checkboxes") @@map("daily_checkboxes")
} }
model UserPreferences { model UserPreferences {
id String @id @default(cuid()) id String @id @default(cuid())
userId String @unique
// Filtres Kanban (JSON)
kanbanFilters Json? kanbanFilters Json?
// Préférences de vue (JSON)
viewPreferences Json? viewPreferences Json?
// Visibilité des colonnes (JSON)
columnVisibility Json? columnVisibility Json?
// Configuration Jira (JSON)
jiraConfig Json? jiraConfig Json?
// Configuration du scheduler Jira
jiraAutoSync Boolean @default(false) jiraAutoSync Boolean @default(false)
jiraSyncInterval String @default("daily") // hourly, daily, weekly jiraSyncInterval String @default("daily")
tfsConfig Json?
tfsAutoSync Boolean @default(false)
tfsSyncInterval String @default("daily")
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("user_preferences") @@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();

View File

@@ -4,8 +4,11 @@
* Usage: tsx scripts/backup-manager.ts [command] [options] * Usage: tsx scripts/backup-manager.ts [command] [options]
*/ */
import { backupService, BackupConfig } from '../src/services/backup'; import {
import { backupScheduler } from '../src/services/backup-scheduler'; backupService,
BackupConfig,
} from '../src/services/data-management/backup';
import { backupScheduler } from '../src/services/data-management/backup-scheduler';
import { formatDateForDisplay } from '../src/lib/date-utils'; import { formatDateForDisplay } from '../src/lib/date-utils';
interface CliOptions { interface CliOptions {
@@ -70,7 +73,10 @@ OPTIONS:
return options; return options;
} }
private async confirmAction(message: string, force?: boolean): Promise<boolean> { private async confirmAction(
message: string,
force?: boolean
): Promise<boolean> {
if (force) return true; if (force) return true;
// Simulation d'une confirmation (en CLI réel, utiliser readline) // Simulation d'une confirmation (en CLI réel, utiliser readline)
@@ -170,12 +176,16 @@ OPTIONS:
} }
private async createBackup(force: boolean = false): Promise<void> { private async createBackup(force: boolean = false): Promise<void> {
console.log('🔄 Création d\'une sauvegarde...'); console.log("🔄 Création d'une sauvegarde...");
const result = await backupService.createBackup('manual', force); const result = await backupService.createBackup('manual', force);
if (result === null) { if (result === null) {
console.log('⏭️ Sauvegarde sautée: Aucun changement détecté depuis la dernière sauvegarde'); console.log(
console.log(' 💡 Utilisez --force pour créer une sauvegarde malgré tout'); '⏭️ Sauvegarde sautée: Aucun changement détecté depuis la dernière sauvegarde'
);
console.log(
' 💡 Utilisez --force pour créer une sauvegarde malgré tout'
);
return; return;
} }
@@ -200,13 +210,17 @@ OPTIONS:
return; return;
} }
console.log(`${'Nom'.padEnd(40)} ${'Taille'.padEnd(10)} ${'Type'.padEnd(12)} ${'Date'}`); console.log(
`${'Nom'.padEnd(40)} ${'Taille'.padEnd(10)} ${'Type'.padEnd(12)} ${'Date'}`
);
console.log('─'.repeat(80)); console.log('─'.repeat(80));
for (const backup of backups) { for (const backup of backups) {
const name = backup.filename.padEnd(40); const name = backup.filename.padEnd(40);
const size = this.formatFileSize(backup.size).padEnd(10); const size = this.formatFileSize(backup.size).padEnd(10);
const type = (backup.type === 'manual' ? 'Manuelle' : 'Automatique').padEnd(12); const type = (
backup.type === 'manual' ? 'Manuelle' : 'Automatique'
).padEnd(12);
const date = this.formatDate(backup.createdAt); const date = this.formatDate(backup.createdAt);
console.log(`${name} ${size} ${type} ${date}`); console.log(`${name} ${size} ${type} ${date}`);
@@ -230,7 +244,10 @@ OPTIONS:
console.log(`✅ Sauvegarde supprimée: ${filename}`); console.log(`✅ Sauvegarde supprimée: ${filename}`);
} }
private async restoreBackup(filename: string, force?: boolean): Promise<void> { private async restoreBackup(
filename: string,
force?: boolean
): Promise<void> {
const confirmed = await this.confirmAction( const confirmed = await this.confirmAction(
`Restaurer la base de données depuis "${filename}" ? ATTENTION: Cela remplacera toutes les données actuelles !`, `Restaurer la base de données depuis "${filename}" ? ATTENTION: Cela remplacera toutes les données actuelles !`,
force force
@@ -247,24 +264,32 @@ OPTIONS:
} }
private async verifyDatabase(): Promise<void> { private async verifyDatabase(): Promise<void> {
console.log('🔍 Vérification de l\'intégrité de la base...'); console.log("🔍 Vérification de l'intégrité de la base...");
await backupService.verifyDatabaseHealth(); await backupService.verifyDatabaseHealth();
console.log('✅ Base de données vérifiée avec succès'); console.log('✅ Base de données vérifiée avec succès');
} }
private async showConfig(): Promise<void> { private async showConfig(): Promise<void> {
const config = backupService.getConfig(); const config = await backupService.getConfig();
const status = backupScheduler.getStatus(); const status = backupScheduler.getStatus();
console.log('⚙️ Configuration des sauvegardes:\n'); console.log('⚙️ Configuration des sauvegardes:\n');
console.log(` Activé: ${config.enabled ? '✅ Oui' : '❌ Non'}`); console.log(
` Activé: ${config.enabled ? '✅ Oui' : '❌ Non'}`
);
console.log(` Fréquence: ${config.interval}`); console.log(` Fréquence: ${config.interval}`);
console.log(` Max sauvegardes: ${config.maxBackups}`); console.log(` Max sauvegardes: ${config.maxBackups}`);
console.log(` Compression: ${config.compression ? '✅ Oui' : '❌ Non'}`); console.log(
` Compression: ${config.compression ? '✅ Oui' : '❌ Non'}`
);
console.log(` Chemin: ${config.backupPath}`); console.log(` Chemin: ${config.backupPath}`);
console.log(`\n📊 Statut du planificateur:`); console.log(`\n📊 Statut du planificateur:`);
console.log(` En cours: ${status.isRunning ? '✅ Oui' : '❌ Non'}`); console.log(
console.log(` Prochaine: ${status.nextBackup ? this.formatDate(status.nextBackup) : 'Non planifiée'}`); ` En cours: ${status.isRunning ? '✅ Oui' : 'Non'}`
);
console.log(
` Prochaine: ${status.nextBackup ? this.formatDate(status.nextBackup) : 'Non planifiée'}`
);
} }
private async setConfig(configString: string): Promise<void> { private async setConfig(configString: string): Promise<void> {
@@ -283,7 +308,9 @@ OPTIONS:
break; break;
case 'interval': case 'interval':
if (!['hourly', 'daily', 'weekly'].includes(value)) { if (!['hourly', 'daily', 'weekly'].includes(value)) {
console.error('❌ Interval invalide. Utilisez: hourly, daily, ou weekly'); console.error(
'❌ Interval invalide. Utilisez: hourly, daily, ou weekly'
);
process.exit(1); process.exit(1);
} }
newConfig.interval = value as BackupConfig['interval']; newConfig.interval = value as BackupConfig['interval'];
@@ -328,10 +355,16 @@ OPTIONS:
const status = backupScheduler.getStatus(); const status = backupScheduler.getStatus();
console.log('📊 Statut du planificateur:\n'); console.log('📊 Statut du planificateur:\n');
console.log(` État: ${status.isRunning ? '✅ Actif' : '❌ Arrêté'}`); console.log(
console.log(` Activé: ${status.isEnabled ? '✅ Oui' : '❌ Non'}`); ` État: ${status.isRunning ? '✅ Actif' : '❌ Arrêté'}`
);
console.log(
` Activé: ${status.isEnabled ? '✅ Oui' : '❌ Non'}`
);
console.log(` Fréquence: ${status.interval}`); console.log(` Fréquence: ${status.interval}`);
console.log(` Prochaine: ${status.nextBackup ? this.formatDate(status.nextBackup) : 'Non planifiée'}`); console.log(
` Prochaine: ${status.nextBackup ? this.formatDate(status.nextBackup) : 'Non planifiée'}`
);
console.log(` Max sauvegardes: ${status.maxBackups}`); console.log(` Max sauvegardes: ${status.maxBackups}`);
} }
} }

140
scripts/cache-monitor.ts Normal file
View File

@@ -0,0 +1,140 @@
#!/usr/bin/env tsx
/**
* Script de monitoring du cache Jira Analytics
* Usage: npm run cache:monitor
*/
import { jiraAnalyticsCache } from '../src/services/integrations/jira/analytics-cache';
import * as readline from 'readline';
function displayCacheStats() {
console.log('\n📊 === STATISTIQUES DU CACHE JIRA ANALYTICS ===');
const stats = jiraAnalyticsCache.getStats();
console.log(`\n📈 Total des entrées: ${stats.totalEntries}`);
if (stats.projects.length === 0) {
console.log('📭 Aucune donnée en cache');
return;
}
console.log('\n📋 Projets en cache:');
stats.projects.forEach((project) => {
const status = project.isExpired ? '❌ EXPIRÉ' : '✅ VALIDE';
console.log(`${project.projectKey}:`);
console.log(` - Âge: ${project.age}`);
console.log(` - TTL: ${project.ttl}`);
console.log(` - Expire dans: ${project.expiresIn}`);
console.log(` - Taille: ${Math.round(project.size / 1024)}KB`);
console.log(` - Statut: ${status}`);
console.log('');
});
}
function displayCacheActions() {
console.log('\n🔧 === ACTIONS DISPONIBLES ===');
console.log('1. Afficher les statistiques');
console.log('2. Forcer le nettoyage');
console.log('3. Invalider tout le cache');
console.log('4. Surveiller en temps réel (Ctrl+C pour arrêter)');
console.log('5. Quitter');
}
async function monitorRealtime() {
console.log('\n👀 Surveillance en temps réel (Ctrl+C pour arrêter)...');
const interval = setInterval(() => {
console.clear();
displayCacheStats();
console.log('\n⏰ Mise à jour toutes les 5 secondes...');
}, 5000);
// Gérer l'arrêt propre
process.on('SIGINT', () => {
clearInterval(interval);
console.log('\n\n👋 Surveillance arrêtée');
process.exit(0);
});
}
async function main() {
console.log('🚀 Cache Monitor Jira Analytics');
const args = process.argv.slice(2);
const command = args[0];
switch (command) {
case 'stats':
displayCacheStats();
break;
case 'cleanup':
console.log('\n🧹 Nettoyage forcé du cache...');
const cleaned = jiraAnalyticsCache.forceCleanup();
console.log(`${cleaned} entrées supprimées`);
break;
case 'clear':
console.log('\n🗑 Invalidation de tout le cache...');
jiraAnalyticsCache.invalidateAll();
console.log('✅ Cache vidé');
break;
case 'monitor':
await monitorRealtime();
break;
default:
displayCacheStats();
displayCacheActions();
// Interface interactive simple
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const askAction = () => {
rl.question(
'\nChoisissez une action (1-5): ',
async (answer: string) => {
switch (answer.trim()) {
case '1':
displayCacheStats();
askAction();
break;
case '2':
const cleaned = jiraAnalyticsCache.forceCleanup();
console.log(`${cleaned} entrées supprimées`);
askAction();
break;
case '3':
jiraAnalyticsCache.invalidateAll();
console.log('✅ Cache vidé');
askAction();
break;
case '4':
rl.close();
await monitorRealtime();
break;
case '5':
console.log('👋 Au revoir !');
rl.close();
process.exit(0);
break;
default:
console.log('❌ Action invalide');
askAction();
}
}
);
};
askAction();
}
}
// Exécution du script
main().catch(console.error);

View File

@@ -0,0 +1,132 @@
#!/usr/bin/env tsx
/**
* Script pour générer les icônes PNG et ICO à partir de iconTC4S.png
* Préserve la transparence du PNG source
*
* Usage: pnpm tsx scripts/generate-icons-from-jpg.ts
*
* Prérequis: npm install -D sharp
*/
import { existsSync } from 'fs';
import { join } from 'path';
const sizes = [16, 32, 180, 192, 512];
const publicDir = join(process.cwd(), 'public');
const sourceImage = join(process.cwd(), 'public', 'icons', 'iconTC4S.png');
async function generateIcons() {
// Vérifier si sharp est disponible
let sharp: any;
try {
sharp = require('sharp');
} catch (e) {
console.log("⚠️ sharp n'est pas installé. Installation...");
console.log(' Exécutez: pnpm add -D sharp');
console.log(' Puis relancez ce script.');
return;
}
if (!existsSync(sourceImage)) {
console.error(`${sourceImage} introuvable`);
return;
}
console.log(
'🎨 Génération des icônes à partir de iconTC4S.png (avec transparence)...\n'
);
// Obtenir les métadonnées de l'image pour détecter la couleur dominante du fond
const metadata = await sharp(sourceImage).metadata();
console.log(`📐 Dimensions source: ${metadata.width}x${metadata.height}\n`);
// Générer les différentes tailles avec cover pour remplir sans bordures
// La transparence est préservée automatiquement avec PNG
for (const size of sizes) {
try {
const outputPath = join(publicDir, `icon-${size}x${size}.png`);
await sharp(sourceImage)
.resize(size, size, {
fit: 'cover',
position: 'center',
})
.png({
compressionLevel: 9,
adaptiveFiltering: true,
palette: false, // Préserve la transparence et les couleurs
})
.toFile(outputPath);
console.log(`✅ Généré: icon-${size}x${size}.png`);
} catch (error) {
console.error(
`❌ Erreur lors de la génération de icon-${size}x${size}.png:`,
error
);
}
}
// Générer apple-touch-icon
try {
const outputPath = join(publicDir, 'apple-touch-icon.png');
await sharp(sourceImage)
.resize(180, 180, {
fit: 'cover',
position: 'center',
})
.png({
compressionLevel: 9,
adaptiveFiltering: true,
palette: false,
})
.toFile(outputPath);
console.log(`✅ Généré: apple-touch-icon.png`);
} catch (error) {
console.error(
`❌ Erreur lors de la génération de apple-touch-icon.png:`,
error
);
}
// Générer favicon.ico (32x32)
// Note: ICO peut supporter la transparence, mais on génère un PNG pour compatibilité
try {
const faviconPath = join(publicDir, 'favicon.ico');
await sharp(sourceImage)
.resize(32, 32, {
fit: 'cover',
position: 'center',
})
.png({
compressionLevel: 9,
adaptiveFiltering: true,
palette: false,
})
.toFile(faviconPath);
console.log(`✅ Généré: favicon.ico (32x32 avec transparence)`);
} catch (error) {
console.error(`❌ Erreur lors de la génération de favicon.ico:`, error);
}
// Générer icon.png (192x192 pour PWA)
try {
const iconPath = join(publicDir, 'icon.png');
await sharp(sourceImage)
.resize(192, 192, {
fit: 'cover',
position: 'center',
})
.png({
compressionLevel: 9,
adaptiveFiltering: true,
palette: false,
})
.toFile(iconPath);
console.log(`✅ Généré: icon.png (192x192)`);
} catch (error) {
console.error(`❌ Erreur lors de la génération de icon.png:`, error);
}
console.log('\n✨ Génération terminée!');
}
generateIcons().catch(console.error);

View File

@@ -1,4 +1,4 @@
import { prisma } from '../src/services/database'; import { prisma } from '../src/services/core/database';
/** /**
* Script pour reset la base de données et supprimer les anciennes données * Script pour reset la base de données et supprimer les anciennes données
@@ -10,8 +10,12 @@ async function resetDatabase() {
try { try {
// Compter les tâches avant suppression // Compter les tâches avant suppression
const beforeCount = await prisma.task.count(); const beforeCount = await prisma.task.count();
const manualCount = await prisma.task.count({ where: { source: 'manual' } }); const manualCount = await prisma.task.count({
const remindersCount = await prisma.task.count({ where: { source: 'reminders' } }); where: { source: 'manual' },
});
const remindersCount = await prisma.task.count({
where: { source: 'reminders' },
});
console.log(`📊 État actuel:`); console.log(`📊 État actuel:`);
console.log(` Total: ${beforeCount} tâches`); console.log(` Total: ${beforeCount} tâches`);
@@ -22,8 +26,8 @@ async function resetDatabase() {
// Supprimer toutes les tâches de synchronisation // Supprimer toutes les tâches de synchronisation
const deletedTasks = await prisma.task.deleteMany({ const deletedTasks = await prisma.task.deleteMany({
where: { where: {
source: 'reminders' source: 'reminders',
} },
}); });
console.log(`✅ Supprimé ${deletedTasks.count} tâches de synchronisation`); console.log(`✅ Supprimé ${deletedTasks.count} tâches de synchronisation`);
@@ -51,30 +55,32 @@ async function resetDatabase() {
include: { include: {
taskTags: { taskTags: {
include: { include: {
tag: true tag: true,
}
}
}, },
orderBy: { createdAt: 'desc' } },
},
orderBy: { createdAt: 'desc' },
}); });
remainingTasks.forEach((task, index) => { remainingTasks.forEach((task, index) => {
const statusEmoji = { const statusEmoji =
'todo': '⏳', {
'in_progress': '🔄', todo: '',
'done': '', in_progress: '🔄',
'cancelled': '❌' done: '✅',
cancelled: '❌',
}[task.status] || '❓'; }[task.status] || '❓';
// Utiliser les relations TaskTag // Utiliser les relations TaskTag
const tags = task.taskTags ? task.taskTags.map(tt => tt.tag.name) : []; const tags = task.taskTags
? task.taskTags.map((tt) => tt.tag.name)
: [];
const tagsStr = tags.length > 0 ? ` [${tags.join(', ')}]` : ''; const tagsStr = tags.length > 0 ? ` [${tags.join(', ')}]` : '';
console.log(` ${index + 1}. ${statusEmoji} ${task.title}${tagsStr}`); console.log(` ${index + 1}. ${statusEmoji} ${task.title}${tagsStr}`);
}); });
} }
} catch (error) { } catch (error) {
console.error('❌ Erreur lors du reset:', error); console.error('❌ Erreur lors du reset:', error);
throw error; throw error;
@@ -83,11 +89,13 @@ async function resetDatabase() {
// Exécuter le script // Exécuter le script
if (require.main === module) { if (require.main === module) {
resetDatabase().then(() => { resetDatabase()
.then(() => {
console.log(''); console.log('');
console.log('✨ Reset terminé avec succès !'); console.log('✨ Reset terminé avec succès !');
process.exit(0); process.exit(0);
}).catch((error) => { })
.catch((error) => {
console.error('💥 Erreur fatale:', error); console.error('💥 Erreur fatale:', error);
process.exit(1); process.exit(1);
}); });

View File

@@ -1,5 +1,6 @@
import { tasksService } from '../src/services/tasks'; import { tasksService } from '../src/services/task-management/tasks';
import { TaskStatus, TaskPriority } from '../src/lib/types'; import { TaskStatus, TaskPriority } from '../src/lib/types';
import { prisma } from '../src/services/core/database';
/** /**
* Script pour ajouter des données de test avec tags et variété * Script pour ajouter des données de test avec tags et variété
@@ -8,22 +9,46 @@ async function seedTestData() {
console.log('🌱 Ajout de données de test...'); console.log('🌱 Ajout de données de test...');
console.log('================================'); console.log('================================');
// Récupérer le premier user ou créer un user temporaire
let userId: string;
const firstUser = await prisma.user.findFirst({
orderBy: { createdAt: 'asc' },
});
if (firstUser) {
userId = firstUser.id;
console.log(`👤 Utilisation du user existant: ${firstUser.email}`);
} else {
// Créer un user temporaire pour les tests
const tempUser = await prisma.user.create({
data: {
email: 'test@example.com',
name: 'Test User',
password: '$2b$10$temp', // Mot de passe temporaire
},
});
userId = tempUser.id;
console.log(`👤 User temporaire créé: ${tempUser.email}`);
}
const testTasks = [ const testTasks = [
{ {
title: '🎨 Design System Implementation', title: '🎨 Design System Implementation',
description: 'Create and implement a comprehensive design system with reusable components', description:
'Create and implement a comprehensive design system with reusable components',
status: 'in_progress' as TaskStatus, status: 'in_progress' as TaskStatus,
priority: 'high' as TaskPriority, priority: 'high' as TaskPriority,
tags: ['design', 'ui', 'frontend'], tags: ['design', 'ui', 'frontend'],
dueDate: new Date('2025-12-31') dueDate: new Date('2025-12-31'),
}, },
{ {
title: '🔧 API Performance Optimization', title: '🔧 API Performance Optimization',
description: 'Optimize API endpoints response time and implement pagination', description:
'Optimize API endpoints response time and implement pagination',
status: 'todo' as TaskStatus, status: 'todo' as TaskStatus,
priority: 'medium' as TaskPriority, priority: 'medium' as TaskPriority,
tags: ['backend', 'performance', 'api'], tags: ['backend', 'performance', 'api'],
dueDate: new Date('2025-12-15') dueDate: new Date('2025-12-15'),
}, },
{ {
title: '✅ Test Coverage Improvement', title: '✅ Test Coverage Improvement',
@@ -31,7 +56,7 @@ async function seedTestData() {
status: 'todo' as TaskStatus, status: 'todo' as TaskStatus,
priority: 'medium' as TaskPriority, priority: 'medium' as TaskPriority,
tags: ['testing', 'quality'], tags: ['testing', 'quality'],
dueDate: new Date('2025-12-20') dueDate: new Date('2025-12-20'),
}, },
{ {
title: '📱 Mobile Responsive Design', title: '📱 Mobile Responsive Design',
@@ -39,7 +64,7 @@ async function seedTestData() {
status: 'todo' as TaskStatus, status: 'todo' as TaskStatus,
priority: 'high' as TaskPriority, priority: 'high' as TaskPriority,
tags: ['frontend', 'mobile', 'ui'], tags: ['frontend', 'mobile', 'ui'],
dueDate: new Date('2025-12-10') dueDate: new Date('2025-12-10'),
}, },
{ {
title: '🔒 Security Audit', title: '🔒 Security Audit',
@@ -47,8 +72,8 @@ async function seedTestData() {
status: 'backlog' as TaskStatus, status: 'backlog' as TaskStatus,
priority: 'urgent' as TaskPriority, priority: 'urgent' as TaskPriority,
tags: ['security', 'audit'], tags: ['security', 'audit'],
dueDate: new Date('2026-01-15') dueDate: new Date('2026-01-15'),
} },
]; ];
let createdCount = 0; let createdCount = 0;
@@ -56,35 +81,43 @@ async function seedTestData() {
for (const taskData of testTasks) { for (const taskData of testTasks) {
try { try {
const task = await tasksService.createTask(taskData); const task = await tasksService.createTask({
...taskData,
ownerId: userId, // Ajouter l'ownerId
});
const statusEmoji = { const statusEmoji = {
'backlog': '📋', backlog: '📋',
'todo': '⏳', todo: '⏳',
'in_progress': '🔄', in_progress: '🔄',
'freeze': '🧊', freeze: '🧊',
'done': '✅', done: '✅',
'cancelled': '❌', cancelled: '❌',
'archived': '📦' archived: '📦',
}[task.status]; }[task.status];
const priorityEmoji = { const priorityEmoji = {
'low': '🔵', low: '🔵',
'medium': '🟡', medium: '🟡',
'high': '🔴', high: '🔴',
'urgent': '🚨' urgent: '🚨',
}[task.priority]; }[task.priority];
console.log(` ${statusEmoji} ${priorityEmoji} ${task.title}`); console.log(` ${statusEmoji} ${priorityEmoji} ${task.title}`);
console.log(` Tags: ${task.tags?.join(', ') || 'aucun'}`); console.log(` Tags: ${task.tags?.join(', ') || 'aucun'}`);
if (task.dueDate) { if (task.dueDate) {
console.log(` Échéance: ${task.dueDate.toLocaleDateString('fr-FR')}`); console.log(
` Échéance: ${task.dueDate.toLocaleDateString('fr-FR')}`
);
} }
console.log(''); console.log('');
createdCount++; createdCount++;
} catch (error) { } catch (error) {
console.error(` ❌ Erreur pour "${taskData.title}":`, error instanceof Error ? error.message : error); console.error(
` ❌ Erreur pour "${taskData.title}":`,
error instanceof Error ? error.message : error
);
errorCount++; errorCount++;
} }
} }
@@ -94,7 +127,7 @@ async function seedTestData() {
console.log(` ❌ Erreurs: ${errorCount}`); console.log(` ❌ Erreurs: ${errorCount}`);
// Afficher les stats finales // Afficher les stats finales
const stats = await tasksService.getTaskStats(); const stats = await tasksService.getTaskStats(userId);
console.log(''); console.log('');
console.log('📈 Statistiques finales:'); console.log('📈 Statistiques finales:');
console.log(` Total: ${stats.total} tâches`); console.log(` Total: ${stats.total} tâches`);
@@ -107,11 +140,13 @@ async function seedTestData() {
// Exécuter le script // Exécuter le script
if (require.main === module) { if (require.main === module) {
seedTestData().then(() => { seedTestData()
.then(() => {
console.log(''); console.log('');
console.log('✨ Données de test ajoutées avec succès !'); console.log('✨ Données de test ajoutées avec succès !');
process.exit(0); process.exit(0);
}).catch((error) => { })
.catch((error) => {
console.error('💥 Erreur fatale:', error); console.error('💥 Erreur fatale:', error);
process.exit(1); process.exit(1);
}); });

View File

@@ -1,7 +1,22 @@
import { tagsService } from '../src/services/tags'; import { PrismaClient } from '@prisma/client';
import { tagsService } from '../src/services/task-management/tags';
const prisma = new PrismaClient();
async function seedTags() { async function seedTags() {
console.log('🏷️ Création des tags de test...'); console.log('🌱 Début du seeding des tags...');
// Récupérer le premier utilisateur pour assigner les tags
const firstUser = await prisma.user.findFirst({
orderBy: { createdAt: 'asc' },
});
if (!firstUser) {
console.log("❌ Aucun utilisateur trouvé. Créez d'abord un utilisateur.");
return;
}
console.log(`👤 Assignation des tags à: ${firstUser.email}`);
const testTags = [ const testTags = [
{ name: 'frontend', color: '#3B82F6' }, { name: 'frontend', color: '#3B82F6' },
@@ -19,9 +34,15 @@ async function seedTags() {
for (const tagData of testTags) { for (const tagData of testTags) {
try { try {
const existing = await tagsService.getTagByName(tagData.name); const existing = await tagsService.getTagByName(
tagData.name,
firstUser.id
);
if (!existing) { if (!existing) {
const tag = await tagsService.createTag(tagData); const tag = await tagsService.createTag({
...tagData,
userId: firstUser.id,
});
console.log(`✅ Tag créé: ${tag.name} (${tag.color})`); console.log(`✅ Tag créé: ${tag.name} (${tag.color})`);
} else { } else {
console.log(`⚠️ Tag existe déjà: ${tagData.name}`); console.log(`⚠️ Tag existe déjà: ${tagData.name}`);

View File

@@ -0,0 +1,93 @@
#!/usr/bin/env tsx
/**
* Script pour identifier les champs personnalisés disponibles dans Jira
* Usage: npm run test:jira-fields
*/
import { JiraService } from '../src/services/integrations/jira/jira';
import { userPreferencesService } from '../src/services/core/user-preferences';
async function testJiraFields() {
console.log('🔍 Identification des champs personnalisés Jira\n');
try {
// Récupérer la config Jira pour l'utilisateur spécifié ou 'default'
const userId = process.argv[2] || 'default';
const jiraConfig = await userPreferencesService.getJiraConfig(userId);
if (
!jiraConfig.enabled ||
!jiraConfig.baseUrl ||
!jiraConfig.email ||
!jiraConfig.apiToken
) {
console.log('❌ Configuration Jira manquante');
return;
}
if (!jiraConfig.projectKey) {
console.log('❌ Aucun projet configuré');
return;
}
console.log(`📋 Analyse du projet: ${jiraConfig.projectKey}`);
// Créer le service Jira
const jiraService = new JiraService(jiraConfig);
// Récupérer un seul ticket pour analyser tous ses champs
const jql = `project = "${jiraConfig.projectKey}" ORDER BY updated DESC`;
const issues = await jiraService.searchIssues(jql);
if (issues.length === 0) {
console.log('❌ Aucun ticket trouvé');
return;
}
const firstIssue = issues[0];
console.log(`\n📄 Analyse du ticket: ${firstIssue.key}`);
console.log(`Titre: ${firstIssue.summary}`);
console.log(`Type: ${firstIssue.issuetype.name}`);
// Afficher les story points actuels
console.log(
`\n🎯 Story Points actuels: ${firstIssue.storyPoints || 'Non défini'}`
);
console.log('\n💡 Pour identifier le bon champ story points:');
console.log('1. Connectez-vous à votre instance Jira');
console.log('2. Allez dans Administration > Projets > [Votre projet]');
console.log('3. Regardez dans "Champs" ou "Story Points"');
console.log(
'4. Notez le nom du champ personnalisé (ex: customfield_10003)'
);
console.log(
'5. Modifiez le code dans src/services/integrations/jira/jira.ts ligne 167'
);
console.log('\n🔧 Champs couramment utilisés pour les story points:');
console.log('• customfield_10002 (par défaut)');
console.log('• customfield_10003');
console.log('• customfield_10004');
console.log('• customfield_10005');
console.log('• customfield_10006');
console.log('• customfield_10007');
console.log('• customfield_10008');
console.log('• customfield_10009');
console.log('• customfield_10010');
console.log('\n📝 Alternative: Utiliser les estimations par type');
console.log('Le système utilise déjà des estimations intelligentes:');
console.log('• Epic: 13 points');
console.log('• Story: 5 points');
console.log('• Task: 3 points');
console.log('• Bug: 2 points');
console.log('• Subtask: 1 point');
} catch (error) {
console.error('❌ Erreur lors du test:', error);
}
}
// Exécution du script
testJiraFields().catch(console.error);

62
scripts/test-runner.js Executable file
View File

@@ -0,0 +1,62 @@
#!/usr/bin/env node
/**
* Script pour gérer PostCSS pendant les tests
* Renomme temporairement postcss.config.mjs pour éviter les erreurs Vitest
*/
const fs = require('fs');
const path = require('path');
const { spawn } = require('child_process');
const postcssConfigPath = path.join(process.cwd(), 'postcss.config.mjs');
const postcssConfigBackupPath = path.join(
process.cwd(),
'postcss.config.mjs.testbak'
);
// Fonction pour restaurer le fichier
function restorePostCSS() {
if (fs.existsSync(postcssConfigBackupPath)) {
fs.renameSync(postcssConfigBackupPath, postcssConfigPath);
console.log('✓ PostCSS config restauré');
}
}
// Renommer le fichier PostCSS avant les tests
if (fs.existsSync(postcssConfigPath)) {
fs.renameSync(postcssConfigPath, postcssConfigBackupPath);
console.log('✓ PostCSS config temporairement désactivé pour les tests');
// Lancer Vitest avec reporter verbose pour plus de détails
const vitest = spawn('pnpm', ['vitest', '--run', '--reporter=verbose'], {
stdio: 'inherit',
});
// Restaurer le fichier après que Vitest ait terminé
vitest.on('close', (code) => {
restorePostCSS();
process.exit(code || 0);
});
// Gérer les signaux d'interruption
process.on('SIGINT', () => {
vitest.kill('SIGINT');
restorePostCSS();
process.exit(0);
});
process.on('SIGTERM', () => {
vitest.kill('SIGTERM');
restorePostCSS();
process.exit(0);
});
} else {
// Si le fichier n'existe pas, lancer Vitest directement
const vitest = spawn('pnpm', ['vitest', '--run'], {
stdio: 'inherit',
});
vitest.on('close', (code) => {
process.exit(code || 0);
});
}

View File

@@ -0,0 +1,130 @@
#!/usr/bin/env tsx
/**
* Script de test pour vérifier la récupération des story points Jira
* Usage: npm run test:story-points
*/
import { JiraService } from '../src/services/integrations/jira/jira';
import { userPreferencesService } from '../src/services/core/user-preferences';
async function testStoryPoints() {
console.log('🧪 Test de récupération des story points Jira\n');
try {
// Récupérer la config Jira pour l'utilisateur spécifié ou 'default'
const userId = process.argv[2] || 'default';
const jiraConfig = await userPreferencesService.getJiraConfig(userId);
if (
!jiraConfig.enabled ||
!jiraConfig.baseUrl ||
!jiraConfig.email ||
!jiraConfig.apiToken
) {
console.log('❌ Configuration Jira manquante');
return;
}
if (!jiraConfig.projectKey) {
console.log('❌ Aucun projet configuré');
return;
}
console.log(`📋 Test sur le projet: ${jiraConfig.projectKey}`);
// Créer le service Jira
const jiraService = new JiraService(jiraConfig);
// Récupérer quelques tickets pour tester
const jql = `project = "${jiraConfig.projectKey}" ORDER BY updated DESC`;
const issues = await jiraService.searchIssues(jql);
console.log(`\n📊 Analyse de ${issues.length} tickets:\n`);
let totalStoryPoints = 0;
let ticketsWithStoryPoints = 0;
let ticketsWithoutStoryPoints = 0;
const storyPointsDistribution: Record<number, number> = {};
const typeDistribution: Record<
string,
{ count: number; totalPoints: number }
> = {};
issues.slice(0, 20).forEach((issue, index) => {
const storyPoints = issue.storyPoints || 0;
const issueType = issue.issuetype.name;
console.log(`${index + 1}. ${issue.key} (${issueType})`);
console.log(` Titre: ${issue.summary.substring(0, 50)}...`);
console.log(
` Story Points: ${storyPoints > 0 ? storyPoints : 'Non défini'}`
);
console.log(` Statut: ${issue.status.name}`);
console.log('');
if (storyPoints > 0) {
ticketsWithStoryPoints++;
totalStoryPoints += storyPoints;
storyPointsDistribution[storyPoints] =
(storyPointsDistribution[storyPoints] || 0) + 1;
} else {
ticketsWithoutStoryPoints++;
}
// Distribution par type
if (!typeDistribution[issueType]) {
typeDistribution[issueType] = { count: 0, totalPoints: 0 };
}
typeDistribution[issueType].count++;
typeDistribution[issueType].totalPoints += storyPoints;
});
console.log('📈 === RÉSUMÉ ===\n');
console.log(`Total tickets analysés: ${issues.length}`);
console.log(`Tickets avec story points: ${ticketsWithStoryPoints}`);
console.log(`Tickets sans story points: ${ticketsWithoutStoryPoints}`);
console.log(`Total story points: ${totalStoryPoints}`);
console.log(
`Moyenne par ticket: ${issues.length > 0 ? (totalStoryPoints / issues.length).toFixed(2) : 0}`
);
console.log('\n📊 Distribution des story points:');
Object.entries(storyPointsDistribution)
.sort(([a], [b]) => parseInt(a) - parseInt(b))
.forEach(([points, count]) => {
console.log(` ${points} points: ${count} tickets`);
});
console.log('\n🏷 Distribution par type:');
Object.entries(typeDistribution)
.sort(([, a], [, b]) => b.count - a.count)
.forEach(([type, stats]) => {
const avgPoints =
stats.count > 0 ? (stats.totalPoints / stats.count).toFixed(2) : '0';
console.log(
` ${type}: ${stats.count} tickets, ${stats.totalPoints} points total, ${avgPoints} points moyen`
);
});
if (ticketsWithoutStoryPoints > 0) {
console.log('\n⚠ Recommandations:');
console.log(
'• Vérifiez que le champ "Story Points" est configuré dans votre projet Jira'
);
console.log('• Le champ par défaut est "customfield_10002"');
console.log(
'• Si votre projet utilise un autre champ, modifiez le code dans jira.ts'
);
console.log(
'• En attendant, le système utilise des estimations basées sur le type de ticket'
);
}
} catch (error) {
console.error('❌ Erreur lors du test:', error);
}
}
// Exécution du script
testStoryPoints().catch(console.error);

View File

@@ -1,25 +0,0 @@
'use server';
import { AnalyticsService, ProductivityMetrics, TimeRange } from '@/services/analytics';
export async function getProductivityMetrics(timeRange?: TimeRange): Promise<{
success: boolean;
data?: ProductivityMetrics;
error?: string;
}> {
try {
const metrics = await AnalyticsService.getProductivityMetrics(timeRange);
return {
success: true,
data: metrics
};
} catch (error) {
console.error('Erreur lors de la récupération des métriques:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
};
}
}

64
src/actions/backup.ts Normal file
View File

@@ -0,0 +1,64 @@
'use server';
import { backupService } from '@/services/data-management/backup';
import { revalidatePath } from 'next/cache';
export async function createBackupAction(force: boolean = false) {
try {
const result = await backupService.createBackup('manual', force);
// Invalider le cache de la page pour forcer le rechargement des données SSR
revalidatePath('/settings/backup');
if (result === null) {
return {
success: true,
skipped: true,
message:
'Sauvegarde sautée : aucun changement détecté. Utilisez "Forcer" pour créer malgré tout.',
};
}
return {
success: true,
data: result,
message: `Sauvegarde créée : ${result.filename}`,
};
} catch (error) {
console.error('Failed to create backup:', error);
return {
success: false,
error:
error instanceof Error
? error.message
: 'Erreur lors de la création de la sauvegarde',
};
}
}
export async function verifyDatabaseAction() {
try {
await backupService.verifyDatabaseHealth();
return {
success: true,
message: 'Intégrité vérifiée',
};
} catch (error) {
console.error('Database verification failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Vérification échouée',
};
}
}
export async function refreshBackupStatsAction() {
try {
// Cette action sert juste à revalider le cache
revalidatePath('/settings/backup');
return { success: true };
} catch (error) {
console.error('Failed to refresh backup stats:', error);
return { success: false };
}
}

View File

@@ -1,9 +1,20 @@
'use server'; 'use server';
import { dailyService } from '@/services/daily'; import { dailyService } from '@/services/task-management/daily';
import { UpdateDailyCheckboxData, DailyCheckbox, CreateDailyCheckboxData } from '@/lib/types'; import {
UpdateDailyCheckboxData,
DailyCheckbox,
CreateDailyCheckboxData,
} from '@/lib/types';
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import { getToday, getPreviousWorkday, parseDate, normalizeDate } from '@/lib/date-utils'; import {
getToday,
getPreviousWorkday,
parseDate,
normalizeDate,
} from '@/lib/date-utils';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/lib/auth';
/** /**
* Toggle l'état d'une checkbox * Toggle l'état d'une checkbox
@@ -14,28 +25,13 @@ export async function toggleCheckbox(checkboxId: string): Promise<{
error?: string; error?: string;
}> { }> {
try { try {
// Nous devons d'abord récupérer la checkbox pour connaître son état actuel const session = await getServerSession(authOptions);
// En absence de getCheckboxById, nous allons essayer de la trouver via une vue daily if (!session?.user?.id) {
// Pour l'instant, nous allons simplement toggle via updateCheckbox return { success: false, error: 'Non authentifié' };
// (le front-end gère déjà l'état optimiste)
// Récupérer toutes les checkboxes d'aujourd'hui et hier pour trouver celle à toggle
const today = getToday();
const dailyView = await dailyService.getDailyView(today);
let checkbox = dailyView.today.find(cb => cb.id === checkboxId);
if (!checkbox) {
checkbox = dailyView.yesterday.find(cb => cb.id === checkboxId);
} }
if (!checkbox) { // Toggle direct côté service par ID (indépendant de la date)
return { success: false, error: 'Checkbox non trouvée' }; const updatedCheckbox = await dailyService.toggleCheckbox(checkboxId);
}
// Toggle l'état
const updatedCheckbox = await dailyService.updateCheckbox(checkboxId, {
isChecked: !checkbox.isChecked
});
revalidatePath('/daily'); revalidatePath('/daily');
return { success: true, data: updatedCheckbox }; return { success: true, data: updatedCheckbox };
@@ -43,36 +39,7 @@ export async function toggleCheckbox(checkboxId: string): Promise<{
console.error('Erreur toggleCheckbox:', error); console.error('Erreur toggleCheckbox:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue' error: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}
/**
* Ajoute une checkbox à une date donnée
*/
export async function addCheckboxToDaily(dailyId: string, content: string, taskId?: string): Promise<{
success: boolean;
data?: DailyCheckbox;
error?: string;
}> {
try {
// Le dailyId correspond à la date au format YYYY-MM-DD
const date = parseDate(dailyId);
const newCheckbox = await dailyService.addCheckbox({
date,
text: content,
taskId
});
revalidatePath('/daily');
return { success: true, data: newCheckbox };
} catch (error) {
console.error('Erreur addCheckboxToDaily:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
}; };
} }
} }
@@ -80,17 +47,30 @@ export async function addCheckboxToDaily(dailyId: string, content: string, taskI
/** /**
* Ajoute une checkbox pour aujourd'hui * Ajoute une checkbox pour aujourd'hui
*/ */
export async function addTodayCheckbox(content: string, type?: 'task' | 'meeting', taskId?: string): Promise<{ export async function addTodayCheckbox(
content: string,
type?: 'task' | 'meeting',
taskId?: string,
date?: Date
): Promise<{
success: boolean; success: boolean;
data?: DailyCheckbox; data?: DailyCheckbox;
error?: string; error?: string;
}> { }> {
try { try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
const targetDate = normalizeDate(date || getToday());
const newCheckbox = await dailyService.addCheckbox({ const newCheckbox = await dailyService.addCheckbox({
date: getToday(), date: targetDate,
userId: session.user.id,
text: content, text: content,
type: type || 'task', type: type || 'task',
taskId taskId,
}); });
revalidatePath('/daily'); revalidatePath('/daily');
@@ -99,7 +79,7 @@ export async function addTodayCheckbox(content: string, type?: 'task' | 'meeting
console.error('Erreur addTodayCheckbox:', error); console.error('Erreur addTodayCheckbox:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue' error: error instanceof Error ? error.message : 'Erreur inconnue',
}; };
} }
} }
@@ -107,19 +87,31 @@ export async function addTodayCheckbox(content: string, type?: 'task' | 'meeting
/** /**
* Ajoute une checkbox pour hier * Ajoute une checkbox pour hier
*/ */
export async function addYesterdayCheckbox(content: string, type?: 'task' | 'meeting', taskId?: string): Promise<{ export async function addYesterdayCheckbox(
content: string,
type?: 'task' | 'meeting',
taskId?: string,
baseDate?: Date
): Promise<{
success: boolean; success: boolean;
data?: DailyCheckbox; data?: DailyCheckbox;
error?: string; error?: string;
}> { }> {
try { try {
const yesterday = getPreviousWorkday(getToday()); const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
const base = normalizeDate(baseDate || getToday());
const yesterday = getPreviousWorkday(base);
const newCheckbox = await dailyService.addCheckbox({ const newCheckbox = await dailyService.addCheckbox({
date: yesterday, date: yesterday,
userId: session.user.id,
text: content, text: content,
type: type || 'task', type: type || 'task',
taskId taskId,
}); });
revalidatePath('/daily'); revalidatePath('/daily');
@@ -128,31 +120,7 @@ export async function addYesterdayCheckbox(content: string, type?: 'task' | 'mee
console.error('Erreur addYesterdayCheckbox:', error); console.error('Erreur addYesterdayCheckbox:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue' error: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}
/**
* Met à jour le contenu d'une checkbox
*/
export async function updateCheckboxContent(checkboxId: string, content: string): Promise<{
success: boolean;
data?: DailyCheckbox;
error?: string;
}> {
try {
const updatedCheckbox = await dailyService.updateCheckbox(checkboxId, {
text: content
});
revalidatePath('/daily');
return { success: true, data: updatedCheckbox };
} catch (error) {
console.error('Erreur updateCheckboxContent:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
}; };
} }
} }
@@ -160,7 +128,10 @@ export async function updateCheckboxContent(checkboxId: string, content: string)
/** /**
* Met à jour une checkbox complète * Met à jour une checkbox complète
*/ */
export async function updateCheckbox(checkboxId: string, data: UpdateDailyCheckboxData): Promise<{ export async function updateCheckbox(
checkboxId: string,
data: UpdateDailyCheckboxData
): Promise<{
success: boolean; success: boolean;
data?: DailyCheckbox; data?: DailyCheckbox;
error?: string; error?: string;
@@ -174,7 +145,7 @@ export async function updateCheckbox(checkboxId: string, data: UpdateDailyCheckb
console.error('Erreur updateCheckbox:', error); console.error('Erreur updateCheckbox:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue' error: error instanceof Error ? error.message : 'Erreur inconnue',
}; };
} }
} }
@@ -195,7 +166,7 @@ export async function deleteCheckbox(checkboxId: string): Promise<{
console.error('Erreur deleteCheckbox:', error); console.error('Erreur deleteCheckbox:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue' error: error instanceof Error ? error.message : 'Erreur inconnue',
}; };
} }
} }
@@ -203,20 +174,30 @@ export async function deleteCheckbox(checkboxId: string): Promise<{
/** /**
* Ajoute un todo lié à une tâche * Ajoute un todo lié à une tâche
*/ */
export async function addTodoToTask(taskId: string, text: string, date?: Date): Promise<{ export async function addTodoToTask(
taskId: string,
text: string,
date?: Date
): Promise<{
success: boolean; success: boolean;
data?: DailyCheckbox; data?: DailyCheckbox;
error?: string; error?: string;
}> { }> {
try { try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
const targetDate = normalizeDate(date || getToday()); const targetDate = normalizeDate(date || getToday());
const checkboxData: CreateDailyCheckboxData = { const checkboxData: CreateDailyCheckboxData = {
date: targetDate, date: targetDate,
userId: session.user.id,
text: text.trim(), text: text.trim(),
type: 'task', type: 'task',
taskId: taskId, taskId: taskId,
isChecked: false isChecked: false,
}; };
const checkbox = await dailyService.addCheckbox(checkboxData); const checkbox = await dailyService.addCheckbox(checkboxData);
@@ -228,7 +209,7 @@ export async function addTodoToTask(taskId: string, text: string, date?: Date):
console.error('Erreur addTodoToTask:', error); console.error('Erreur addTodoToTask:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue' error: error instanceof Error ? error.message : 'Erreur inconnue',
}; };
} }
} }
@@ -236,7 +217,10 @@ export async function addTodoToTask(taskId: string, text: string, date?: Date):
/** /**
* Réorganise les checkboxes d'une date * Réorganise les checkboxes d'une date
*/ */
export async function reorderCheckboxes(dailyId: string, checkboxIds: string[]): Promise<{ export async function reorderCheckboxes(
dailyId: string,
checkboxIds: string[]
): Promise<{
success: boolean; success: boolean;
error?: string; error?: string;
}> { }> {
@@ -252,7 +236,29 @@ export async function reorderCheckboxes(dailyId: string, checkboxIds: string[]):
console.error('Erreur reorderCheckboxes:', error); console.error('Erreur reorderCheckboxes:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue' error: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}
/**
* Déplace une checkbox non cochée à aujourd'hui
*/
export async function moveCheckboxToToday(checkboxId: string): Promise<{
success: boolean;
data?: DailyCheckbox;
error?: string;
}> {
try {
const updatedCheckbox = await dailyService.moveCheckboxToToday(checkboxId);
revalidatePath('/daily');
return { success: true, data: updatedCheckbox };
} catch (error) {
console.error('Erreur moveCheckboxToToday:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue',
}; };
} }
} }

View File

@@ -1,8 +1,10 @@
'use server'; 'use server';
import { JiraAnalyticsService } from '@/services/jira-analytics'; import { JiraAnalyticsService } from '@/services/integrations/jira/analytics';
import { userPreferencesService } from '@/services/user-preferences'; import { userPreferencesService } from '@/services/core/user-preferences';
import { JiraAnalytics } from '@/lib/types'; import { JiraAnalytics } from '@/lib/types';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
export type JiraAnalyticsResult = { export type JiraAnalyticsResult = {
success: boolean; success: boolean;
@@ -13,31 +15,48 @@ export type JiraAnalyticsResult = {
/** /**
* Server Action pour récupérer les analytics Jira du projet configuré * Server Action pour récupérer les analytics Jira du projet configuré
*/ */
export async function getJiraAnalytics(forceRefresh = false): Promise<JiraAnalyticsResult> { export async function getJiraAnalytics(
forceRefresh = false
): Promise<JiraAnalyticsResult> {
try { try {
// Récupérer la config Jira depuis la base de données const session = await getServerSession(authOptions);
const jiraConfig = await userPreferencesService.getJiraConfig(); if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
if (!jiraConfig.enabled || !jiraConfig.baseUrl || !jiraConfig.email || !jiraConfig.apiToken) { // Récupérer la config Jira depuis la base de données
const jiraConfig = await userPreferencesService.getJiraConfig(
session.user.id
);
if (
!jiraConfig.enabled ||
!jiraConfig.baseUrl ||
!jiraConfig.email ||
!jiraConfig.apiToken
) {
return { return {
success: false, success: false,
error: 'Configuration Jira manquante. Configurez Jira dans les paramètres.' error:
'Configuration Jira manquante. Configurez Jira dans les paramètres.',
}; };
} }
if (!jiraConfig.projectKey) { if (!jiraConfig.projectKey) {
return { return {
success: false, success: false,
error: 'Aucun projet configuré pour les analytics. Configurez un projet dans les paramètres Jira.' error:
'Aucun projet configuré pour les analytics. Configurez un projet dans les paramètres Jira.',
}; };
} }
// Créer le service d'analytics // Créer le service d'analytics
const analyticsService = new JiraAnalyticsService({ const analyticsService = new JiraAnalyticsService({
enabled: jiraConfig.enabled,
baseUrl: jiraConfig.baseUrl, baseUrl: jiraConfig.baseUrl,
email: jiraConfig.email, email: jiraConfig.email,
apiToken: jiraConfig.apiToken, apiToken: jiraConfig.apiToken,
projectKey: jiraConfig.projectKey projectKey: jiraConfig.projectKey,
}); });
// Récupérer les analytics (avec cache ou actualisation forcée) // Récupérer les analytics (avec cache ou actualisation forcée)
@@ -45,15 +64,17 @@ export async function getJiraAnalytics(forceRefresh = false): Promise<JiraAnalyt
return { return {
success: true, success: true,
data: analytics data: analytics,
}; };
} catch (error) { } catch (error) {
console.error('❌ Erreur lors du calcul des analytics Jira:', error); console.error('❌ Erreur lors du calcul des analytics Jira:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Erreur lors du calcul des analytics' error:
error instanceof Error
? error.message
: 'Erreur lors du calcul des analytics',
}; };
} }
} }

View File

@@ -1,8 +1,17 @@
'use server'; 'use server';
import { jiraAnomalyDetection, JiraAnomaly, AnomalyDetectionConfig } from '@/services/jira-anomaly-detection'; import {
import { JiraAnalyticsService, JiraAnalyticsConfig } from '@/services/jira-analytics'; jiraAnomalyDetection,
import { userPreferencesService } from '@/services/user-preferences'; JiraAnomaly,
AnomalyDetectionConfig,
} from '@/services/integrations/jira/anomaly-detection';
import {
JiraAnalyticsService,
JiraAnalyticsConfig,
} from '@/services/integrations/jira/analytics';
import { userPreferencesService } from '@/services/core/user-preferences';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
export interface AnomalyDetectionResult { export interface AnomalyDetectionResult {
success: boolean; success: boolean;
@@ -13,15 +22,29 @@ export interface AnomalyDetectionResult {
/** /**
* Détecte les anomalies dans les métriques Jira actuelles * Détecte les anomalies dans les métriques Jira actuelles
*/ */
export async function detectJiraAnomalies(forceRefresh = false): Promise<AnomalyDetectionResult> { export async function detectJiraAnomalies(
forceRefresh = false
): Promise<AnomalyDetectionResult> {
try { try {
// Récupérer la config Jira const session = await getServerSession(authOptions);
const jiraConfig = await userPreferencesService.getJiraConfig(); if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
if (!jiraConfig?.baseUrl || !jiraConfig?.email || !jiraConfig?.apiToken || !jiraConfig?.projectKey) { // Récupérer la config Jira
const jiraConfig = await userPreferencesService.getJiraConfig(
session.user.id
);
if (
!jiraConfig?.baseUrl ||
!jiraConfig?.email ||
!jiraConfig?.apiToken ||
!jiraConfig?.projectKey
) {
return { return {
success: false, success: false,
error: 'Configuration Jira incomplète' error: 'Configuration Jira incomplète',
}; };
} }
@@ -30,7 +53,9 @@ export async function detectJiraAnomalies(forceRefresh = false): Promise<Anomaly
return { success: false, error: 'Configuration Jira incomplète' }; return { success: false, error: 'Configuration Jira incomplète' };
} }
const analyticsService = new JiraAnalyticsService(jiraConfig as JiraAnalyticsConfig); const analyticsService = new JiraAnalyticsService(
jiraConfig as JiraAnalyticsConfig
);
const analytics = await analyticsService.getProjectAnalytics(forceRefresh); const analytics = await analyticsService.getProjectAnalytics(forceRefresh);
// Détecter les anomalies // Détecter les anomalies
@@ -38,13 +63,13 @@ export async function detectJiraAnomalies(forceRefresh = false): Promise<Anomaly
return { return {
success: true, success: true,
data: anomalies data: anomalies,
}; };
} catch (error) { } catch (error) {
console.error('❌ Erreur lors de la détection d\'anomalies:', error); console.error("❌ Erreur lors de la détection d'anomalies:", error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue' error: error instanceof Error ? error.message : 'Erreur inconnue',
}; };
} }
} }
@@ -52,19 +77,21 @@ export async function detectJiraAnomalies(forceRefresh = false): Promise<Anomaly
/** /**
* Met à jour la configuration de détection d'anomalies * Met à jour la configuration de détection d'anomalies
*/ */
export async function updateAnomalyDetectionConfig(config: Partial<AnomalyDetectionConfig>) { export async function updateAnomalyDetectionConfig(
config: Partial<AnomalyDetectionConfig>
) {
try { try {
jiraAnomalyDetection.updateConfig(config); jiraAnomalyDetection.updateConfig(config);
return { return {
success: true, success: true,
data: jiraAnomalyDetection.getConfig() data: jiraAnomalyDetection.getConfig(),
}; };
} catch (error) { } catch (error) {
console.error('❌ Erreur lors de la mise à jour de la config:', error); console.error('❌ Erreur lors de la mise à jour de la config:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue' error: error instanceof Error ? error.message : 'Erreur inconnue',
}; };
} }
} }
@@ -76,13 +103,13 @@ export async function getAnomalyDetectionConfig() {
try { try {
return { return {
success: true, success: true,
data: jiraAnomalyDetection.getConfig() data: jiraAnomalyDetection.getConfig(),
}; };
} catch (error) { } catch (error) {
console.error('❌ Erreur lors de la récupération de la config:', error); console.error('❌ Erreur lors de la récupération de la config:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue' error: error instanceof Error ? error.message : 'Erreur inconnue',
}; };
} }
} }

View File

@@ -1,98 +0,0 @@
'use server';
import { jiraAnalyticsCache } from '@/services/jira-analytics-cache';
import { userPreferencesService } from '@/services/user-preferences';
export type CacheStatsResult = {
success: boolean;
data?: {
totalEntries: number;
projects: Array<{ projectKey: string; age: string; size: number }>;
};
error?: string;
};
export type CacheActionResult = {
success: boolean;
message?: string;
error?: string;
};
/**
* Server Action pour récupérer les statistiques du cache
*/
export async function getJiraCacheStats(): Promise<CacheStatsResult> {
try {
const stats = jiraAnalyticsCache.getStats();
return {
success: true,
data: stats
};
} catch (error) {
console.error('❌ Erreur lors de la récupération des stats du cache:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
};
}
}
/**
* Server Action pour invalider le cache du projet configuré
*/
export async function invalidateJiraCache(): Promise<CacheActionResult> {
try {
// Récupérer la config Jira actuelle
const jiraConfig = await userPreferencesService.getJiraConfig();
if (!jiraConfig.enabled || !jiraConfig.baseUrl || !jiraConfig.email || !jiraConfig.apiToken || !jiraConfig.projectKey) {
return {
success: false,
error: 'Configuration Jira incomplète'
};
}
// Invalider le cache pour ce projet
jiraAnalyticsCache.invalidate({
baseUrl: jiraConfig.baseUrl,
email: jiraConfig.email,
apiToken: jiraConfig.apiToken,
projectKey: jiraConfig.projectKey
});
return {
success: true,
message: `Cache invalidé pour le projet ${jiraConfig.projectKey}`
};
} catch (error) {
console.error('❌ Erreur lors de l\'invalidation du cache:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
};
}
}
/**
* Server Action pour invalider tout le cache analytics
*/
export async function invalidateAllJiraCache(): Promise<CacheActionResult> {
try {
jiraAnalyticsCache.invalidateAll();
return {
success: true,
message: 'Tout le cache analytics a été invalidé'
};
} catch (error) {
console.error('❌ Erreur lors de l\'invalidation totale du cache:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
};
}
}

View File

@@ -91,7 +91,9 @@ export interface JiraAnalytics {
/** /**
* Server Action pour exporter les analytics Jira au format CSV ou JSON * Server Action pour exporter les analytics Jira au format CSV ou JSON
*/ */
export async function exportJiraAnalytics(format: ExportFormat = 'csv'): Promise<ExportResult> { export async function exportJiraAnalytics(
format: ExportFormat = 'csv'
): Promise<ExportResult> {
try { try {
// Récupérer les analytics (force refresh pour avoir les données les plus récentes) // Récupérer les analytics (force refresh pour avoir les données les plus récentes)
const analyticsResult = await getJiraAnalytics(true); const analyticsResult = await getJiraAnalytics(true);
@@ -99,7 +101,7 @@ export async function exportJiraAnalytics(format: ExportFormat = 'csv'): Promise
if (!analyticsResult.success || !analyticsResult.data) { if (!analyticsResult.success || !analyticsResult.data) {
return { return {
success: false, success: false,
error: analyticsResult.error || 'Impossible de récupérer les analytics' error: analyticsResult.error || 'Impossible de récupérer les analytics',
}; };
} }
@@ -111,7 +113,7 @@ export async function exportJiraAnalytics(format: ExportFormat = 'csv'): Promise
return { return {
success: true, success: true,
data: JSON.stringify(analytics, null, 2), data: JSON.stringify(analytics, null, 2),
filename: `jira-analytics-${projectKey}-${timestamp}.json` filename: `jira-analytics-${projectKey}-${timestamp}.json`,
}; };
} }
@@ -121,15 +123,14 @@ export async function exportJiraAnalytics(format: ExportFormat = 'csv'): Promise
return { return {
success: true, success: true,
data: csvData, data: csvData,
filename: `jira-analytics-${projectKey}-${timestamp}.csv` filename: `jira-analytics-${projectKey}-${timestamp}.csv`,
}; };
} catch (error) { } catch (error) {
console.error('❌ Erreur lors de l\'export des analytics:', error); console.error("❌ Erreur lors de l'export des analytics:", error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue' error: error instanceof Error ? error.message : 'Erreur inconnue',
}; };
} }
} }
@@ -143,103 +144,126 @@ function generateCSV(analytics: JiraAnalytics): string {
// Header du rapport // Header du rapport
lines.push('# Rapport Analytics Jira'); lines.push('# Rapport Analytics Jira');
lines.push(`# Projet: ${analytics.project.name} (${analytics.project.key})`); lines.push(`# Projet: ${analytics.project.name} (${analytics.project.key})`);
lines.push(`# Généré le: ${formatDateForDisplay(getToday(), 'DISPLAY_LONG')}`); lines.push(
`# Généré le: ${formatDateForDisplay(getToday(), 'DISPLAY_LONG')}`
);
lines.push(`# Total tickets: ${analytics.project.totalIssues}`); lines.push(`# Total tickets: ${analytics.project.totalIssues}`);
lines.push(''); lines.push('');
// Section 1: Métriques d'équipe // Section 1: Métriques d'équipe
lines.push('## Répartition de l\'équipe'); lines.push("## Répartition de l'équipe");
lines.push('Assignee,Nom,Total Tickets,Tickets Complétés,Tickets En Cours,Pourcentage'); lines.push(
analytics.teamMetrics.issuesDistribution.forEach((assignee: AssigneeMetrics) => { 'Assignee,Nom,Total Tickets,Tickets Complétés,Tickets En Cours,Pourcentage'
lines.push([ );
analytics.teamMetrics.issuesDistribution.forEach(
(assignee: AssigneeMetrics) => {
lines.push(
[
escapeCsv(assignee.assignee), escapeCsv(assignee.assignee),
escapeCsv(assignee.displayName), escapeCsv(assignee.displayName),
assignee.totalIssues, assignee.totalIssues,
assignee.completedIssues, assignee.completedIssues,
assignee.inProgressIssues, assignee.inProgressIssues,
assignee.percentage.toFixed(1) + '%' assignee.percentage.toFixed(1) + '%',
].join(',')); ].join(',')
}); );
}
);
lines.push(''); lines.push('');
// Section 2: Historique des sprints // Section 2: Historique des sprints
lines.push('## Historique des sprints'); lines.push('## Historique des sprints');
lines.push('Sprint,Date Début,Date Fin,Points Planifiés,Points Complétés,Taux de Complétion'); lines.push(
'Sprint,Date Début,Date Fin,Points Planifiés,Points Complétés,Taux de Complétion'
);
analytics.velocityMetrics.sprintHistory.forEach((sprint: SprintHistory) => { analytics.velocityMetrics.sprintHistory.forEach((sprint: SprintHistory) => {
lines.push([ lines.push(
[
escapeCsv(sprint.sprintName), escapeCsv(sprint.sprintName),
sprint.startDate.slice(0, 10), sprint.startDate.slice(0, 10),
sprint.endDate.slice(0, 10), sprint.endDate.slice(0, 10),
sprint.plannedPoints, sprint.plannedPoints,
sprint.completedPoints, sprint.completedPoints,
sprint.completionRate + '%' sprint.completionRate + '%',
].join(',')); ].join(',')
);
}); });
lines.push(''); lines.push('');
// Section 3: Cycle time par type // Section 3: Cycle time par type
lines.push('## Cycle Time par type de ticket'); lines.push('## Cycle Time par type de ticket');
lines.push('Type de Ticket,Temps Moyen (jours),Temps Médian (jours),Échantillons'); lines.push(
analytics.cycleTimeMetrics.cycleTimeByType.forEach((type: CycleTimeByType) => { 'Type de Ticket,Temps Moyen (jours),Temps Médian (jours),Échantillons'
lines.push([ );
analytics.cycleTimeMetrics.cycleTimeByType.forEach(
(type: CycleTimeByType) => {
lines.push(
[
escapeCsv(type.issueType), escapeCsv(type.issueType),
type.averageDays, type.averageDays,
type.medianDays, type.medianDays,
type.samples type.samples,
].join(',')); ].join(',')
}); );
}
);
lines.push(''); lines.push('');
// Section 4: Work in Progress // Section 4: Work in Progress
lines.push('## Work in Progress par statut'); lines.push('## Work in Progress par statut');
lines.push('Statut,Nombre,Pourcentage'); lines.push('Statut,Nombre,Pourcentage');
analytics.workInProgress.byStatus.forEach((status: WorkInProgressStatus) => { analytics.workInProgress.byStatus.forEach((status: WorkInProgressStatus) => {
lines.push([ lines.push(
escapeCsv(status.status), [escapeCsv(status.status), status.count, status.percentage + '%'].join(
status.count, ','
status.percentage + '%' )
].join(',')); );
}); });
lines.push(''); lines.push('');
// Section 5: Charge de travail par assignee // Section 5: Charge de travail par assignee
lines.push('## Charge de travail par assignee'); lines.push('## Charge de travail par assignee');
lines.push('Assignee,Nom,À Faire,En Cours,En Revue,Total Actif'); lines.push('Assignee,Nom,À Faire,En Cours,En Revue,Total Actif');
analytics.workInProgress.byAssignee.forEach((assignee: WorkInProgressAssignee) => { analytics.workInProgress.byAssignee.forEach(
lines.push([ (assignee: WorkInProgressAssignee) => {
lines.push(
[
escapeCsv(assignee.assignee), escapeCsv(assignee.assignee),
escapeCsv(assignee.displayName), escapeCsv(assignee.displayName),
assignee.todoCount, assignee.todoCount,
assignee.inProgressCount, assignee.inProgressCount,
assignee.reviewCount, assignee.reviewCount,
assignee.totalActive assignee.totalActive,
].join(',')); ].join(',')
}); );
}
);
lines.push(''); lines.push('');
// Section 6: Métriques résumé // Section 6: Métriques résumé
lines.push('## Métriques de résumé'); lines.push('## Métriques de résumé');
lines.push('Métrique,Valeur'); lines.push('Métrique,Valeur');
lines.push([ lines.push(
'Total membres équipe', ['Total membres équipe', analytics.teamMetrics.totalAssignees].join(',')
analytics.teamMetrics.totalAssignees );
].join(',')); lines.push(
lines.push([ ['Membres actifs', analytics.teamMetrics.activeAssignees].join(',')
'Membres actifs', );
analytics.teamMetrics.activeAssignees lines.push(
].join(',')); [
lines.push([
'Points complétés sprint actuel', 'Points complétés sprint actuel',
analytics.velocityMetrics.currentSprintPoints analytics.velocityMetrics.currentSprintPoints,
].join(',')); ].join(',')
lines.push([ );
'Vélocité moyenne', lines.push(
analytics.velocityMetrics.averageVelocity ['Vélocité moyenne', analytics.velocityMetrics.averageVelocity].join(',')
].join(',')); );
lines.push([ lines.push(
[
'Cycle time moyen (jours)', 'Cycle time moyen (jours)',
analytics.cycleTimeMetrics.averageCycleTime analytics.cycleTimeMetrics.averageCycleTime,
].join(',')); ].join(',')
);
return lines.join('\n'); return lines.join('\n');
} }

View File

@@ -1,9 +1,18 @@
'use server'; 'use server';
import { JiraAnalyticsService, JiraAnalyticsConfig } from '@/services/jira-analytics'; import {
import { JiraAdvancedFiltersService } from '@/services/jira-advanced-filters'; JiraAnalyticsService,
import { userPreferencesService } from '@/services/user-preferences'; JiraAnalyticsConfig,
import { AvailableFilters, JiraAnalyticsFilters, JiraAnalytics } from '@/lib/types'; } from '@/services/integrations/jira/analytics';
import { JiraAdvancedFiltersService } from '@/services/integrations/jira/advanced-filters';
import { userPreferencesService } from '@/services/core/user-preferences';
import {
AvailableFilters,
JiraAnalyticsFilters,
JiraAnalytics,
} from '@/lib/types';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
export interface FiltersResult { export interface FiltersResult {
success: boolean; success: boolean;
@@ -22,13 +31,25 @@ export interface FilteredAnalyticsResult {
*/ */
export async function getAvailableJiraFilters(): Promise<FiltersResult> { export async function getAvailableJiraFilters(): Promise<FiltersResult> {
try { try {
// Récupérer la config Jira const session = await getServerSession(authOptions);
const jiraConfig = await userPreferencesService.getJiraConfig(); if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
if (!jiraConfig?.baseUrl || !jiraConfig?.email || !jiraConfig?.apiToken || !jiraConfig?.projectKey) { // Récupérer la config Jira
const jiraConfig = await userPreferencesService.getJiraConfig(
session.user.id
);
if (
!jiraConfig?.baseUrl ||
!jiraConfig?.email ||
!jiraConfig?.apiToken ||
!jiraConfig?.projectKey
) {
return { return {
success: false, success: false,
error: 'Configuration Jira incomplète' error: 'Configuration Jira incomplète',
}; };
} }
@@ -37,23 +58,26 @@ export async function getAvailableJiraFilters(): Promise<FiltersResult> {
return { success: false, error: 'Configuration Jira incomplète' }; return { success: false, error: 'Configuration Jira incomplète' };
} }
const analyticsService = new JiraAnalyticsService(jiraConfig as JiraAnalyticsConfig); const analyticsService = new JiraAnalyticsService(
jiraConfig as JiraAnalyticsConfig
);
// Récupérer la liste des issues pour extraire les filtres // Récupérer la liste des issues pour extraire les filtres
const allIssues = await analyticsService.getAllProjectIssues(); const allIssues = await analyticsService.getAllProjectIssues();
// Extraire les filtres disponibles // Extraire les filtres disponibles
const availableFilters = JiraAdvancedFiltersService.extractAvailableFilters(allIssues); const availableFilters =
JiraAdvancedFiltersService.extractAvailableFilters(allIssues);
return { return {
success: true, success: true,
data: availableFilters data: availableFilters,
}; };
} catch (error) { } catch (error) {
console.error('❌ Erreur lors de la récupération des filtres:', error); console.error('❌ Erreur lors de la récupération des filtres:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue' error: error instanceof Error ? error.message : 'Erreur inconnue',
}; };
} }
} }
@@ -61,15 +85,29 @@ export async function getAvailableJiraFilters(): Promise<FiltersResult> {
/** /**
* Applique des filtres aux analytics et retourne les données filtrées * Applique des filtres aux analytics et retourne les données filtrées
*/ */
export async function getFilteredJiraAnalytics(filters: Partial<JiraAnalyticsFilters>): Promise<FilteredAnalyticsResult> { export async function getFilteredJiraAnalytics(
filters: Partial<JiraAnalyticsFilters>
): Promise<FilteredAnalyticsResult> {
try { try {
// Récupérer la config Jira const session = await getServerSession(authOptions);
const jiraConfig = await userPreferencesService.getJiraConfig(); if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
if (!jiraConfig?.baseUrl || !jiraConfig?.email || !jiraConfig?.apiToken || !jiraConfig?.projectKey) { // Récupérer la config Jira
const jiraConfig = await userPreferencesService.getJiraConfig(
session.user.id
);
if (
!jiraConfig?.baseUrl ||
!jiraConfig?.email ||
!jiraConfig?.apiToken ||
!jiraConfig?.projectKey
) {
return { return {
success: false, success: false,
error: 'Configuration Jira incomplète' error: 'Configuration Jira incomplète',
}; };
} }
@@ -78,14 +116,16 @@ export async function getFilteredJiraAnalytics(filters: Partial<JiraAnalyticsFil
return { success: false, error: 'Configuration Jira incomplète' }; return { success: false, error: 'Configuration Jira incomplète' };
} }
const analyticsService = new JiraAnalyticsService(jiraConfig as JiraAnalyticsConfig); const analyticsService = new JiraAnalyticsService(
jiraConfig as JiraAnalyticsConfig
);
const originalAnalytics = await analyticsService.getProjectAnalytics(); const originalAnalytics = await analyticsService.getProjectAnalytics();
// Si aucun filtre actif, retourner les données originales // Si aucun filtre actif, retourner les données originales
if (!JiraAdvancedFiltersService.hasActiveFilters(filters)) { if (!JiraAdvancedFiltersService.hasActiveFilters(filters)) {
return { return {
success: true, success: true,
data: originalAnalytics data: originalAnalytics,
}; };
} }
@@ -93,7 +133,8 @@ export async function getFilteredJiraAnalytics(filters: Partial<JiraAnalyticsFil
const allIssues = await analyticsService.getAllProjectIssues(); const allIssues = await analyticsService.getAllProjectIssues();
// Appliquer les filtres // Appliquer les filtres
const filteredAnalytics = JiraAdvancedFiltersService.applyFiltersToAnalytics( const filteredAnalytics =
JiraAdvancedFiltersService.applyFiltersToAnalytics(
originalAnalytics, originalAnalytics,
filters, filters,
allIssues allIssues
@@ -101,13 +142,13 @@ export async function getFilteredJiraAnalytics(filters: Partial<JiraAnalyticsFil
return { return {
success: true, success: true,
data: filteredAnalytics data: filteredAnalytics,
}; };
} catch (error) { } catch (error) {
console.error('❌ Erreur lors du filtrage des analytics:', error); console.error('❌ Erreur lors du filtrage des analytics:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue' error: error instanceof Error ? error.message : 'Erreur inconnue',
}; };
} }
} }

View File

@@ -1,10 +1,20 @@
'use server'; 'use server';
import { JiraAnalyticsService, JiraAnalyticsConfig } from '@/services/jira-analytics'; import {
import { userPreferencesService } from '@/services/user-preferences'; JiraAnalyticsService,
JiraAnalyticsConfig,
} from '@/services/integrations/jira/analytics';
import { userPreferencesService } from '@/services/core/user-preferences';
import { SprintDetails } from '@/components/jira/SprintDetailModal'; import { SprintDetails } from '@/components/jira/SprintDetailModal';
import { JiraTask, AssigneeDistribution, StatusDistribution, SprintVelocity } from '@/lib/types'; import {
JiraTask,
AssigneeDistribution,
StatusDistribution,
SprintVelocity,
} from '@/lib/types';
import { parseDate } from '@/lib/date-utils'; import { parseDate } from '@/lib/date-utils';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
export interface SprintDetailsResult { export interface SprintDetailsResult {
success: boolean; success: boolean;
@@ -15,15 +25,29 @@ export interface SprintDetailsResult {
/** /**
* Récupère les détails d'un sprint spécifique * Récupère les détails d'un sprint spécifique
*/ */
export async function getSprintDetails(sprintName: string): Promise<SprintDetailsResult> { export async function getSprintDetails(
sprintName: string
): Promise<SprintDetailsResult> {
try { try {
// Récupérer la config Jira const session = await getServerSession(authOptions);
const jiraConfig = await userPreferencesService.getJiraConfig(); if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
if (!jiraConfig?.baseUrl || !jiraConfig?.email || !jiraConfig?.apiToken || !jiraConfig?.projectKey) { // Récupérer la config Jira
const jiraConfig = await userPreferencesService.getJiraConfig(
session.user.id
);
if (
!jiraConfig?.baseUrl ||
!jiraConfig?.email ||
!jiraConfig?.apiToken ||
!jiraConfig?.projectKey
) {
return { return {
success: false, success: false,
error: 'Configuration Jira incomplète' error: 'Configuration Jira incomplète',
}; };
} }
@@ -32,14 +56,18 @@ export async function getSprintDetails(sprintName: string): Promise<SprintDetail
return { success: false, error: 'Configuration Jira incomplète' }; return { success: false, error: 'Configuration Jira incomplète' };
} }
const analyticsService = new JiraAnalyticsService(jiraConfig as JiraAnalyticsConfig); const analyticsService = new JiraAnalyticsService(
jiraConfig as JiraAnalyticsConfig
);
const analytics = await analyticsService.getProjectAnalytics(); const analytics = await analyticsService.getProjectAnalytics();
const sprint = analytics.velocityMetrics.sprintHistory.find(s => s.sprintName === sprintName); const sprint = analytics.velocityMetrics.sprintHistory.find(
(s) => s.sprintName === sprintName
);
if (!sprint) { if (!sprint) {
return { return {
success: false, success: false,
error: `Sprint "${sprintName}" introuvable` error: `Sprint "${sprintName}" introuvable`,
}; };
} }
@@ -52,7 +80,7 @@ export async function getSprintDetails(sprintName: string): Promise<SprintDetail
const sprintStart = parseDate(sprint.startDate); const sprintStart = parseDate(sprint.startDate);
const sprintEnd = parseDate(sprint.endDate); const sprintEnd = parseDate(sprint.endDate);
const sprintIssues = allIssues.filter(issue => { const sprintIssues = allIssues.filter((issue) => {
const issueDate = parseDate(issue.created); const issueDate = parseDate(issue.created);
return issueDate >= sprintStart && issueDate <= sprintEnd; return issueDate >= sprintStart && issueDate <= sprintEnd;
}); });
@@ -71,18 +99,21 @@ export async function getSprintDetails(sprintName: string): Promise<SprintDetail
issues: sprintIssues, issues: sprintIssues,
assigneeDistribution, assigneeDistribution,
statusDistribution, statusDistribution,
metrics: sprintMetrics metrics: sprintMetrics,
}; };
return { return {
success: true, success: true,
data: sprintDetails data: sprintDetails,
}; };
} catch (error) { } catch (error) {
console.error('❌ Erreur lors de la récupération des détails du sprint:', error); console.error(
'❌ Erreur lors de la récupération des détails du sprint:',
error
);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue' error: error instanceof Error ? error.message : 'Erreur inconnue',
}; };
} }
} }
@@ -92,25 +123,29 @@ export async function getSprintDetails(sprintName: string): Promise<SprintDetail
*/ */
function calculateSprintMetrics(issues: JiraTask[], sprint: SprintVelocity) { function calculateSprintMetrics(issues: JiraTask[], sprint: SprintVelocity) {
const totalIssues = issues.length; const totalIssues = issues.length;
const completedIssues = issues.filter(issue => const completedIssues = issues.filter(
(issue) =>
issue.status.category === 'Done' || issue.status.category === 'Done' ||
issue.status.name.toLowerCase().includes('done') || issue.status.name.toLowerCase().includes('done') ||
issue.status.name.toLowerCase().includes('closed') issue.status.name.toLowerCase().includes('closed')
).length; ).length;
const inProgressIssues = issues.filter(issue => const inProgressIssues = issues.filter(
(issue) =>
issue.status.category === 'In Progress' || issue.status.category === 'In Progress' ||
issue.status.name.toLowerCase().includes('progress') || issue.status.name.toLowerCase().includes('progress') ||
issue.status.name.toLowerCase().includes('review') issue.status.name.toLowerCase().includes('review')
).length; ).length;
const blockedIssues = issues.filter(issue => const blockedIssues = issues.filter(
(issue) =>
issue.status.name.toLowerCase().includes('blocked') || issue.status.name.toLowerCase().includes('blocked') ||
issue.status.name.toLowerCase().includes('waiting') issue.status.name.toLowerCase().includes('waiting')
).length; ).length;
// Calcul du cycle time moyen pour ce sprint // Calcul du cycle time moyen pour ce sprint
const completedIssuesWithDates = issues.filter(issue => const completedIssuesWithDates = issues.filter(
(issue) =>
issue.status.category === 'Done' && issue.created && issue.updated issue.status.category === 'Done' && issue.created && issue.updated
); );
@@ -119,7 +154,8 @@ function calculateSprintMetrics(issues: JiraTask[], sprint: SprintVelocity) {
const totalCycleTime = completedIssuesWithDates.reduce((total, issue) => { const totalCycleTime = completedIssuesWithDates.reduce((total, issue) => {
const created = parseDate(issue.created); const created = parseDate(issue.created);
const updated = parseDate(issue.updated); const updated = parseDate(issue.updated);
const cycleTime = (updated.getTime() - created.getTime()) / (1000 * 60 * 60 * 24); // en jours const cycleTime =
(updated.getTime() - created.getTime()) / (1000 * 60 * 60 * 24); // en jours
return total + cycleTime; return total + cycleTime;
}, 0); }, 0);
averageCycleTime = totalCycleTime / completedIssuesWithDates.length; averageCycleTime = totalCycleTime / completedIssuesWithDates.length;
@@ -139,19 +175,28 @@ function calculateSprintMetrics(issues: JiraTask[], sprint: SprintVelocity) {
inProgressIssues, inProgressIssues,
blockedIssues, blockedIssues,
averageCycleTime, averageCycleTime,
velocityTrend velocityTrend,
}; };
} }
/** /**
* Calcule la distribution par assigné pour le sprint * Calcule la distribution par assigné pour le sprint
*/ */
function calculateAssigneeDistribution(issues: JiraTask[]): AssigneeDistribution[] { function calculateAssigneeDistribution(
const assigneeMap = new Map<string, { total: number; completed: number; inProgress: number }>(); issues: JiraTask[]
): AssigneeDistribution[] {
const assigneeMap = new Map<
string,
{ total: number; completed: number; inProgress: number }
>();
issues.forEach(issue => { issues.forEach((issue) => {
const assigneeName = issue.assignee?.displayName || 'Non assigné'; const assigneeName = issue.assignee?.displayName || 'Non assigné';
const current = assigneeMap.get(assigneeName) || { total: 0, completed: 0, inProgress: 0 }; const current = assigneeMap.get(assigneeName) || {
total: 0,
completed: 0,
inProgress: 0,
};
current.total++; current.total++;
@@ -164,14 +209,17 @@ function calculateAssigneeDistribution(issues: JiraTask[]): AssigneeDistribution
assigneeMap.set(assigneeName, current); assigneeMap.set(assigneeName, current);
}); });
return Array.from(assigneeMap.entries()).map(([displayName, stats]) => ({ return Array.from(assigneeMap.entries())
.map(([displayName, stats]) => ({
assignee: displayName === 'Non assigné' ? '' : displayName, assignee: displayName === 'Non assigné' ? '' : displayName,
displayName, displayName,
totalIssues: stats.total, totalIssues: stats.total,
completedIssues: stats.completed, completedIssues: stats.completed,
inProgressIssues: stats.inProgress, inProgressIssues: stats.inProgress,
percentage: issues.length > 0 ? (stats.total / issues.length) * 100 : 0 percentage: issues.length > 0 ? (stats.total / issues.length) * 100 : 0,
})).sort((a, b) => b.totalIssues - a.totalIssues); count: stats.total, // Ajout pour compatibilité
}))
.sort((a, b) => b.totalIssues - a.totalIssues);
} }
/** /**
@@ -180,13 +228,18 @@ function calculateAssigneeDistribution(issues: JiraTask[]): AssigneeDistribution
function calculateStatusDistribution(issues: JiraTask[]): StatusDistribution[] { function calculateStatusDistribution(issues: JiraTask[]): StatusDistribution[] {
const statusMap = new Map<string, number>(); const statusMap = new Map<string, number>();
issues.forEach(issue => { issues.forEach((issue) => {
statusMap.set(issue.status.name, (statusMap.get(issue.status.name) || 0) + 1); statusMap.set(
issue.status.name,
(statusMap.get(issue.status.name) || 0) + 1
);
}); });
return Array.from(statusMap.entries()).map(([status, count]) => ({ return Array.from(statusMap.entries())
.map(([status, count]) => ({
status, status,
count, count,
percentage: issues.length > 0 ? (count / issues.length) * 100 : 0 percentage: issues.length > 0 ? (count / issues.length) * 100 : 0,
})).sort((a, b) => b.count - a.count); }))
.sort((a, b) => b.count - a.count);
} }

View File

@@ -1,8 +1,13 @@
'use server'; 'use server';
import { MetricsService, WeeklyMetricsOverview, VelocityTrend } from '@/services/metrics'; import {
MetricsService,
WeeklyMetricsOverview,
VelocityTrend,
} from '@/services/analytics/metrics';
import { getToday } from '@/lib/date-utils'; import { getToday } from '@/lib/date-utils';
import { revalidatePath } from 'next/cache'; import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
/** /**
* Récupère les métriques hebdomadaires pour une date donnée * Récupère les métriques hebdomadaires pour une date donnée
@@ -13,18 +18,33 @@ export async function getWeeklyMetrics(date?: Date): Promise<{
error?: string; error?: string;
}> { }> {
try { try {
// Récupérer l'utilisateur connecté
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return {
success: false,
error: 'Utilisateur non authentifié',
};
}
const targetDate = date || getToday(); const targetDate = date || getToday();
const metrics = await MetricsService.getWeeklyMetrics(targetDate); const metrics = await MetricsService.getWeeklyMetrics(
session.user.id,
targetDate
);
return { return {
success: true, success: true,
data: metrics data: metrics,
}; };
} catch (error) { } catch (error) {
console.error('Error fetching weekly metrics:', error); console.error('Error fetching weekly metrics:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Failed to fetch weekly metrics' error:
error instanceof Error
? error.message
: 'Failed to fetch weekly metrics',
}; };
} }
} }
@@ -38,42 +58,39 @@ export async function getVelocityTrends(weeksBack: number = 4): Promise<{
error?: string; error?: string;
}> { }> {
try { try {
if (weeksBack < 1 || weeksBack > 12) { // Récupérer l'utilisateur connecté
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { return {
success: false, success: false,
error: 'Invalid weeksBack parameter (must be 1-12)' error: 'Utilisateur non authentifié',
}; };
} }
const trends = await MetricsService.getVelocityTrends(weeksBack); if (weeksBack < 1 || weeksBack > 12) {
return {
success: false,
error: 'Invalid weeksBack parameter (must be 1-12)',
};
}
const trends = await MetricsService.getVelocityTrends(
session.user.id,
weeksBack
);
return { return {
success: true, success: true,
data: trends data: trends,
}; };
} catch (error) { } catch (error) {
console.error('Error fetching velocity trends:', error); console.error('Error fetching velocity trends:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Failed to fetch velocity trends' error:
}; error instanceof Error
} ? error.message
} : 'Failed to fetch velocity trends',
/**
* Rafraîchir les données de métriques (invalide le cache)
*/
export async function refreshMetrics(): Promise<{
success: boolean;
error?: string;
}> {
try {
revalidatePath('/manager');
return { success: true };
} catch {
return {
success: false,
error: 'Failed to refresh metrics'
}; };
} }
} }

View File

@@ -1,25 +1,72 @@
'use server'; 'use server';
import { userPreferencesService } from '@/services/user-preferences'; import { userPreferencesService } from '@/services/core/user-preferences';
import { KanbanFilters, ViewPreferences, ColumnVisibility, TaskStatus } from '@/lib/types'; import {
KanbanFilters,
ViewPreferences,
ColumnVisibility,
TaskStatus,
} from '@/lib/types';
import { Theme } from '@/lib/ui-config';
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
/** /**
* Met à jour les préférences de vue * Met à jour les préférences de vue
*/ */
export async function updateViewPreferences(updates: Partial<ViewPreferences>): Promise<{ export async function updateViewPreferences(
updates: Partial<ViewPreferences>
): Promise<{
success: boolean; success: boolean;
error?: string; error?: string;
}> { }> {
try { try {
await userPreferencesService.updateViewPreferences(updates); const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
await userPreferencesService.updateViewPreferences(
session.user.id,
updates
);
revalidatePath('/'); revalidatePath('/');
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Erreur updateViewPreferences:', error); console.error('Erreur updateViewPreferences:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue' error: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}
/**
* Met à jour l'image de fond
*/
export async function setBackgroundImage(
backgroundImage: string | undefined
): Promise<{
success: boolean;
error?: string;
}> {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
await userPreferencesService.updateViewPreferences(session.user.id, {
backgroundImage,
});
revalidatePath('/');
return { success: true };
} catch (error) {
console.error('Erreur setBackgroundImage:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue',
}; };
} }
} }
@@ -27,19 +74,26 @@ export async function updateViewPreferences(updates: Partial<ViewPreferences>):
/** /**
* Met à jour les filtres Kanban * Met à jour les filtres Kanban
*/ */
export async function updateKanbanFilters(updates: Partial<KanbanFilters>): Promise<{ export async function updateKanbanFilters(
updates: Partial<KanbanFilters>
): Promise<{
success: boolean; success: boolean;
error?: string; error?: string;
}> { }> {
try { try {
await userPreferencesService.updateKanbanFilters(updates); const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
await userPreferencesService.updateKanbanFilters(session.user.id, updates);
revalidatePath('/kanban'); revalidatePath('/kanban');
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Erreur updateKanbanFilters:', error); console.error('Erreur updateKanbanFilters:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue' error: error instanceof Error ? error.message : 'Erreur inconnue',
}; };
} }
} }
@@ -47,25 +101,37 @@ export async function updateKanbanFilters(updates: Partial<KanbanFilters>): Prom
/** /**
* Met à jour la visibilité des colonnes * Met à jour la visibilité des colonnes
*/ */
export async function updateColumnVisibility(updates: Partial<ColumnVisibility>): Promise<{ export async function updateColumnVisibility(
updates: Partial<ColumnVisibility>
): Promise<{
success: boolean; success: boolean;
error?: string; error?: string;
}> { }> {
try { try {
const preferences = await userPreferencesService.getAllPreferences(); const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
const preferences = await userPreferencesService.getAllPreferences(
session.user.id
);
const newColumnVisibility: ColumnVisibility = { const newColumnVisibility: ColumnVisibility = {
...preferences.columnVisibility, ...preferences.columnVisibility,
...updates ...updates,
}; };
await userPreferencesService.saveColumnVisibility(newColumnVisibility); await userPreferencesService.saveColumnVisibility(
session.user.id,
newColumnVisibility
);
revalidatePath('/kanban'); revalidatePath('/kanban');
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Erreur updateColumnVisibility:', error); console.error('Erreur updateColumnVisibility:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue' error: error instanceof Error ? error.message : 'Erreur inconnue',
}; };
} }
} }
@@ -78,17 +144,26 @@ export async function toggleObjectivesVisibility(): Promise<{
error?: string; error?: string;
}> { }> {
try { try {
const preferences = await userPreferencesService.getAllPreferences(); const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
const preferences = await userPreferencesService.getAllPreferences(
session.user.id
);
const showObjectives = !preferences.viewPreferences.showObjectives; const showObjectives = !preferences.viewPreferences.showObjectives;
await userPreferencesService.updateViewPreferences({ showObjectives }); await userPreferencesService.updateViewPreferences(session.user.id, {
showObjectives,
});
revalidatePath('/'); revalidatePath('/');
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Erreur toggleObjectivesVisibility:', error); console.error('Erreur toggleObjectivesVisibility:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue' error: error instanceof Error ? error.message : 'Erreur inconnue',
}; };
} }
} }
@@ -101,37 +176,53 @@ export async function toggleObjectivesCollapse(): Promise<{
error?: string; error?: string;
}> { }> {
try { try {
const preferences = await userPreferencesService.getAllPreferences(); const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
const preferences = await userPreferencesService.getAllPreferences(
session.user.id
);
const collapseObjectives = !preferences.viewPreferences.collapseObjectives; const collapseObjectives = !preferences.viewPreferences.collapseObjectives;
await userPreferencesService.updateViewPreferences({ collapseObjectives }); await userPreferencesService.updateViewPreferences(session.user.id, {
collapseObjectives,
});
revalidatePath('/'); revalidatePath('/');
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Erreur toggleObjectivesCollapse:', error); console.error('Erreur toggleObjectivesCollapse:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue' error: error instanceof Error ? error.message : 'Erreur inconnue',
}; };
} }
} }
/** /**
* Change le thème (light/dark) * Change le thème (light/dark/dracula/monokai/nord)
*/ */
export async function setTheme(theme: 'light' | 'dark'): Promise<{ export async function setTheme(theme: Theme): Promise<{
success: boolean; success: boolean;
error?: string; error?: string;
}> { }> {
try { try {
await userPreferencesService.updateViewPreferences({ theme }); const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
await userPreferencesService.updateViewPreferences(session.user.id, {
theme,
});
revalidatePath('/'); revalidatePath('/');
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Erreur setTheme:', error); console.error('Erreur setTheme:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue' error: error instanceof Error ? error.message : 'Erreur inconnue',
}; };
} }
} }
@@ -144,17 +235,27 @@ export async function toggleTheme(): Promise<{
error?: string; error?: string;
}> { }> {
try { try {
const preferences = await userPreferencesService.getAllPreferences(); const session = await getServerSession(authOptions);
const newTheme = preferences.viewPreferences.theme === 'dark' ? 'light' : 'dark'; if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
await userPreferencesService.updateViewPreferences({ theme: newTheme }); const preferences = await userPreferencesService.getAllPreferences(
session.user.id
);
const newTheme =
preferences.viewPreferences.theme === 'dark' ? 'light' : 'dark';
await userPreferencesService.updateViewPreferences(session.user.id, {
theme: newTheme,
});
revalidatePath('/'); revalidatePath('/');
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Erreur toggleTheme:', error); console.error('Erreur toggleTheme:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue' error: error instanceof Error ? error.message : 'Erreur inconnue',
}; };
} }
} }
@@ -167,20 +268,35 @@ export async function toggleFontSize(): Promise<{
error?: string; error?: string;
}> { }> {
try { try {
const preferences = await userPreferencesService.getAllPreferences(); const session = await getServerSession(authOptions);
const fontSizes: ('small' | 'medium' | 'large')[] = ['small', 'medium', 'large']; if (!session?.user?.id) {
const currentIndex = fontSizes.indexOf(preferences.viewPreferences.fontSize); return { success: false, error: 'Non authentifié' };
}
const preferences = await userPreferencesService.getAllPreferences(
session.user.id
);
const fontSizes: ('small' | 'medium' | 'large')[] = [
'small',
'medium',
'large',
];
const currentIndex = fontSizes.indexOf(
preferences.viewPreferences.fontSize
);
const nextIndex = (currentIndex + 1) % fontSizes.length; const nextIndex = (currentIndex + 1) % fontSizes.length;
const newFontSize = fontSizes[nextIndex]; const newFontSize = fontSizes[nextIndex];
await userPreferencesService.updateViewPreferences({ fontSize: newFontSize }); await userPreferencesService.updateViewPreferences(session.user.id, {
fontSize: newFontSize,
});
revalidatePath('/'); revalidatePath('/');
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Erreur toggleFontSize:', error); console.error('Erreur toggleFontSize:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue' error: error instanceof Error ? error.message : 'Erreur inconnue',
}; };
} }
} }
@@ -193,7 +309,14 @@ export async function toggleColumnVisibility(status: TaskStatus): Promise<{
error?: string; error?: string;
}> { }> {
try { try {
const preferences = await userPreferencesService.getAllPreferences(); const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
const preferences = await userPreferencesService.getAllPreferences(
session.user.id
);
const hiddenStatuses = new Set(preferences.columnVisibility.hiddenStatuses); const hiddenStatuses = new Set(preferences.columnVisibility.hiddenStatuses);
if (hiddenStatuses.has(status)) { if (hiddenStatuses.has(status)) {
@@ -202,8 +325,8 @@ export async function toggleColumnVisibility(status: TaskStatus): Promise<{
hiddenStatuses.add(status); hiddenStatuses.add(status);
} }
await userPreferencesService.saveColumnVisibility({ await userPreferencesService.saveColumnVisibility(session.user.id, {
hiddenStatuses: Array.from(hiddenStatuses) hiddenStatuses: Array.from(hiddenStatuses),
}); });
revalidatePath('/kanban'); revalidatePath('/kanban');
@@ -212,7 +335,7 @@ export async function toggleColumnVisibility(status: TaskStatus): Promise<{
console.error('Erreur toggleColumnVisibility:', error); console.error('Erreur toggleColumnVisibility:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue' error: error instanceof Error ? error.message : 'Erreur inconnue',
}; };
} }
} }

176
src/actions/profile.ts Normal file
View File

@@ -0,0 +1,176 @@
'use server';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/lib/auth';
import { usersService } from '@/services/users';
import { revalidatePath } from 'next/cache';
import { getGravatarUrl } from '@/lib/gravatar';
export async function updateProfile(formData: {
name?: string;
firstName?: string;
lastName?: string;
avatar?: string;
useGravatar?: boolean;
}) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
// Validation
if (formData.firstName && formData.firstName.length > 50) {
return {
success: false,
error: 'Le prénom ne peut pas dépasser 50 caractères',
};
}
if (formData.lastName && formData.lastName.length > 50) {
return {
success: false,
error: 'Le nom ne peut pas dépasser 50 caractères',
};
}
if (formData.name && formData.name.length > 100) {
return {
success: false,
error: "Le nom d'affichage ne peut pas dépasser 100 caractères",
};
}
if (formData.avatar && formData.avatar.length > 500) {
return {
success: false,
error: "L'URL de l'avatar ne peut pas dépasser 500 caractères",
};
}
// Déterminer l'URL de l'avatar
let finalAvatarUrl: string | null = null;
if (formData.useGravatar) {
// Utiliser Gravatar si demandé
finalAvatarUrl = getGravatarUrl(session.user.email || '', { size: 200 });
} else if (formData.avatar) {
// Utiliser l'URL custom si fournie
finalAvatarUrl = formData.avatar;
} else {
// Garder l'avatar actuel ou null
const currentUser = await usersService.getUserById(session.user.id);
finalAvatarUrl = currentUser?.avatar || null;
}
// Mettre à jour l'utilisateur
const updatedUser = await usersService.updateUser(session.user.id, {
name: formData.name || null,
firstName: formData.firstName || null,
lastName: formData.lastName || null,
avatar: finalAvatarUrl,
});
// Revalider la page de profil
revalidatePath('/profile');
return {
success: true,
user: {
id: updatedUser.id,
email: updatedUser.email,
name: updatedUser.name,
firstName: updatedUser.firstName,
lastName: updatedUser.lastName,
avatar: updatedUser.avatar,
role: updatedUser.role,
createdAt: updatedUser.createdAt.toISOString(),
lastLoginAt: updatedUser.lastLoginAt?.toISOString() || null,
},
};
} catch (error) {
console.error('Profile update error:', error);
return { success: false, error: 'Erreur lors de la mise à jour du profil' };
}
}
export async function getProfile() {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
const user = await usersService.getUserById(session.user.id);
if (!user) {
return { success: false, error: 'Utilisateur non trouvé' };
}
return {
success: true,
user: {
id: user.id,
email: user.email,
name: user.name,
firstName: user.firstName,
lastName: user.lastName,
avatar: user.avatar,
role: user.role,
createdAt: user.createdAt.toISOString(),
lastLoginAt: user.lastLoginAt?.toISOString() || null,
},
};
} catch (error) {
console.error('Profile get error:', error);
return {
success: false,
error: 'Erreur lors de la récupération du profil',
};
}
}
export async function applyGravatar() {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
if (!session.user?.email) {
return { success: false, error: 'Email requis pour Gravatar' };
}
// Générer l'URL Gravatar
const gravatarUrl = getGravatarUrl(session.user.email, { size: 200 });
// Mettre à jour l'utilisateur
const updatedUser = await usersService.updateUser(session.user.id, {
avatar: gravatarUrl,
});
// Revalider la page de profil
revalidatePath('/profile');
return {
success: true,
user: {
id: updatedUser.id,
email: updatedUser.email,
name: updatedUser.name,
firstName: updatedUser.firstName,
lastName: updatedUser.lastName,
avatar: updatedUser.avatar,
role: updatedUser.role,
createdAt: updatedUser.createdAt.toISOString(),
lastLoginAt: updatedUser.lastLoginAt?.toISOString() || null,
},
};
} catch (error) {
console.error('Gravatar update error:', error);
return { success: false, error: 'Erreur lors de la mise à jour Gravatar' };
}
}

View File

@@ -1,6 +1,6 @@
'use server'; 'use server';
import { SystemInfoService } from '@/services/system-info'; import { SystemInfoService } from '@/services/core/system-info';
export async function getSystemInfo() { export async function getSystemInfo() {
try { try {
@@ -10,7 +10,8 @@ export async function getSystemInfo() {
console.error('Error getting system info:', error); console.error('Error getting system info:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Failed to get system info' error:
error instanceof Error ? error.message : 'Failed to get system info',
}; };
} }
} }

View File

@@ -1,8 +1,10 @@
'use server'; 'use server';
import { tagsService } from '@/services/tags'; import { tagsService } from '@/services/task-management/tags';
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import { Tag } from '@/lib/types'; import { Tag } from '@/lib/types';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/lib/auth';
export type ActionResult<T = void> = { export type ActionResult<T = void> = {
success: boolean; success: boolean;
@@ -18,7 +20,16 @@ export async function createTag(
color: string color: string
): Promise<ActionResult<Tag>> { ): Promise<ActionResult<Tag>> {
try { try {
const tag = await tagsService.createTag({ name, color }); const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'User not authenticated' };
}
const tag = await tagsService.createTag({
name,
color,
userId: session.user.id,
});
// Revalider les pages qui utilisent les tags // Revalider les pages qui utilisent les tags
revalidatePath('/'); revalidatePath('/');
@@ -30,7 +41,7 @@ export async function createTag(
console.error('Error creating tag:', error); console.error('Error creating tag:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Failed to create tag' error: error instanceof Error ? error.message : 'Failed to create tag',
}; };
} }
} }
@@ -43,7 +54,12 @@ export async function updateTag(
data: { name?: string; color?: string; isPinned?: boolean } data: { name?: string; color?: string; isPinned?: boolean }
): Promise<ActionResult<Tag>> { ): Promise<ActionResult<Tag>> {
try { try {
const tag = await tagsService.updateTag(tagId, data); const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'User not authenticated' };
}
const tag = await tagsService.updateTag(tagId, session.user.id, data);
if (!tag) { if (!tag) {
return { success: false, error: 'Tag non trouvé' }; return { success: false, error: 'Tag non trouvé' };
@@ -59,7 +75,7 @@ export async function updateTag(
console.error('Error updating tag:', error); console.error('Error updating tag:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Failed to update tag' error: error instanceof Error ? error.message : 'Failed to update tag',
}; };
} }
} }
@@ -69,7 +85,12 @@ export async function updateTag(
*/ */
export async function deleteTag(tagId: string): Promise<ActionResult> { export async function deleteTag(tagId: string): Promise<ActionResult> {
try { try {
await tagsService.deleteTag(tagId); const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'User not authenticated' };
}
await tagsService.deleteTag(tagId, session.user.id);
// Revalider les pages qui utilisent les tags // Revalider les pages qui utilisent les tags
revalidatePath('/'); revalidatePath('/');
@@ -81,21 +102,7 @@ export async function deleteTag(tagId: string): Promise<ActionResult> {
console.error('Error deleting tag:', error); console.error('Error deleting tag:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Failed to delete tag' error: error instanceof Error ? error.message : 'Failed to delete tag',
}; };
} }
} }
/**
* Action rapide pour créer un tag depuis un input
*/
export async function quickCreateTag(formData: FormData): Promise<ActionResult<Tag>> {
const name = formData.get('name') as string;
const color = formData.get('color') as string;
if (!name?.trim()) {
return { success: false, error: 'Tag name is required' };
}
return createTag(name.trim(), color || '#3B82F6');
}

View File

@@ -1,8 +1,10 @@
'use server' 'use server';
import { tasksService } from '@/services/tasks'; import { tasksService } from '@/services/task-management/tasks';
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import { TaskStatus, TaskPriority } from '@/lib/types'; import { TaskStatus, TaskPriority } from '@/lib/types';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/lib/auth';
export type ActionResult<T = unknown> = { export type ActionResult<T = unknown> = {
success: boolean; success: boolean;
@@ -10,6 +12,30 @@ export type ActionResult<T = unknown> = {
error?: string; error?: string;
}; };
/**
* Helper pour vérifier l'authentification
*/
async function getAuthenticatedUser() {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
throw new Error('Non authentifié');
}
return session.user.id;
}
/**
* Helper pour vérifier qu'une tâche appartient au user connecté
*/
async function verifyTaskOwnership(taskId: string): Promise<boolean> {
try {
const userId = await getAuthenticatedUser();
const tasks = await tasksService.getTasks(userId);
return tasks.some((t) => t.id === taskId);
} catch {
return false;
}
}
/** /**
* Server Action pour mettre à jour le statut d'une tâche * Server Action pour mettre à jour le statut d'une tâche
*/ */
@@ -18,18 +44,30 @@ export async function updateTaskStatus(
status: TaskStatus status: TaskStatus
): Promise<ActionResult> { ): Promise<ActionResult> {
try { try {
const task = await tasksService.updateTask(taskId, { status }); // Vérifier l'authentification et récupérer l'ID du user
const userId = await getAuthenticatedUser();
// Vérifier que la tâche appartient au user connecté
const isOwner = await verifyTaskOwnership(taskId);
if (!isOwner) {
return { success: false, error: 'Tâche non trouvée ou non autorisée' };
}
const updatedTask = await tasksService.updateTask(userId, taskId, {
status,
});
// Revalidation automatique du cache // Revalidation automatique du cache
revalidatePath('/'); revalidatePath('/');
revalidatePath('/tasks'); revalidatePath('/tasks');
return { success: true, data: task }; return { success: true, data: updatedTask };
} catch (error) { } catch (error) {
console.error('Error updating task status:', error); console.error('Error updating task status:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Failed to update task status' error:
error instanceof Error ? error.message : 'Failed to update task status',
}; };
} }
} }
@@ -46,7 +84,18 @@ export async function updateTaskTitle(
return { success: false, error: 'Title cannot be empty' }; return { success: false, error: 'Title cannot be empty' };
} }
const task = await tasksService.updateTask(taskId, { title: title.trim() }); // Vérifier l'authentification et récupérer l'ID du user
const userId = await getAuthenticatedUser();
// Vérifier que la tâche appartient au user connecté
const isOwner = await verifyTaskOwnership(taskId);
if (!isOwner) {
return { success: false, error: 'Tâche non trouvée ou non autorisée' };
}
const task = await tasksService.updateTask(userId, taskId, {
title: title.trim(),
});
// Revalidation automatique du cache // Revalidation automatique du cache
revalidatePath('/'); revalidatePath('/');
@@ -57,7 +106,8 @@ export async function updateTaskTitle(
console.error('Error updating task title:', error); console.error('Error updating task title:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Failed to update task title' error:
error instanceof Error ? error.message : 'Failed to update task title',
}; };
} }
} }
@@ -67,7 +117,16 @@ export async function updateTaskTitle(
*/ */
export async function deleteTask(taskId: string): Promise<ActionResult> { export async function deleteTask(taskId: string): Promise<ActionResult> {
try { try {
await tasksService.deleteTask(taskId); // Vérifier l'authentification et récupérer l'ID du user
const userId = await getAuthenticatedUser();
// Vérifier que la tâche appartient au user connecté
const isOwner = await verifyTaskOwnership(taskId);
if (!isOwner) {
return { success: false, error: 'Tâche non trouvée ou non autorisée' };
}
await tasksService.deleteTask(userId, taskId);
// Revalidation automatique du cache // Revalidation automatique du cache
revalidatePath('/'); revalidatePath('/');
@@ -78,7 +137,7 @@ export async function deleteTask(taskId: string): Promise<ActionResult> {
console.error('Error deleting task:', error); console.error('Error deleting task:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Failed to delete task' error: error instanceof Error ? error.message : 'Failed to delete task',
}; };
} }
} }
@@ -93,9 +152,19 @@ export async function updateTask(data: {
status?: TaskStatus; status?: TaskStatus;
priority?: TaskPriority; priority?: TaskPriority;
tags?: string[]; tags?: string[];
primaryTagId?: string;
dueDate?: Date; dueDate?: Date;
}): Promise<ActionResult> { }): Promise<ActionResult> {
try { try {
// Vérifier l'authentification et récupérer l'ID du user
const userId = await getAuthenticatedUser();
// Vérifier que la tâche appartient au user connecté
const isOwner = await verifyTaskOwnership(data.taskId);
if (!isOwner) {
return { success: false, error: 'Tâche non trouvée ou non autorisée' };
}
const updateData: Record<string, unknown> = {}; const updateData: Record<string, unknown> = {};
if (data.title !== undefined) { if (data.title !== undefined) {
@@ -105,13 +174,16 @@ export async function updateTask(data: {
updateData.title = data.title.trim(); updateData.title = data.title.trim();
} }
if (data.description !== undefined) updateData.description = data.description.trim(); if (data.description !== undefined)
updateData.description = data.description.trim();
if (data.status !== undefined) updateData.status = data.status; if (data.status !== undefined) updateData.status = data.status;
if (data.priority !== undefined) updateData.priority = data.priority; if (data.priority !== undefined) updateData.priority = data.priority;
if (data.tags !== undefined) updateData.tags = data.tags; if (data.tags !== undefined) updateData.tags = data.tags;
if (data.primaryTagId !== undefined)
updateData.primaryTagId = data.primaryTagId;
if (data.dueDate !== undefined) updateData.dueDate = data.dueDate; if (data.dueDate !== undefined) updateData.dueDate = data.dueDate;
const task = await tasksService.updateTask(data.taskId, updateData); const task = await tasksService.updateTask(userId, data.taskId, updateData);
// Revalidation automatique du cache // Revalidation automatique du cache
revalidatePath('/'); revalidatePath('/');
@@ -122,7 +194,7 @@ export async function updateTask(data: {
console.error('Error updating task:', error); console.error('Error updating task:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Failed to update task' error: error instanceof Error ? error.message : 'Failed to update task',
}; };
} }
} }
@@ -136,18 +208,24 @@ export async function createTask(data: {
status?: TaskStatus; status?: TaskStatus;
priority?: TaskPriority; priority?: TaskPriority;
tags?: string[]; tags?: string[];
primaryTagId?: string;
}): Promise<ActionResult> { }): Promise<ActionResult> {
try { try {
if (!data.title.trim()) { if (!data.title.trim()) {
return { success: false, error: 'Title is required' }; return { success: false, error: 'Title is required' };
} }
// Vérifier l'authentification et récupérer l'ID du user
const userId = await getAuthenticatedUser();
const task = await tasksService.createTask({ const task = await tasksService.createTask({
title: data.title.trim(), title: data.title.trim(),
description: data.description?.trim() || '', description: data.description?.trim() || '',
status: data.status || 'todo', status: data.status || 'todo',
priority: data.priority || 'medium', priority: data.priority || 'medium',
tags: data.tags || [] tags: data.tags || [],
primaryTagId: data.primaryTagId,
ownerId: userId, // Assigner la tâche au user connecté
}); });
// Revalidation automatique du cache // Revalidation automatique du cache
@@ -159,7 +237,7 @@ export async function createTask(data: {
console.error('Error creating task:', error); console.error('Error creating task:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Failed to create task' error: error instanceof Error ? error.message : 'Failed to create task',
}; };
} }
} }

181
src/actions/tfs.ts Normal file
View File

@@ -0,0 +1,181 @@
'use server';
import { userPreferencesService } from '@/services/core/user-preferences';
import { revalidatePath } from 'next/cache';
import { tfsService, TfsConfig } from '@/services/integrations/tfs/tfs';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
/**
* Sauvegarde la configuration TFS
*/
export async function saveTfsConfig(config: TfsConfig) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
await userPreferencesService.saveTfsConfig(session.user.id, config);
// Réinitialiser le service pour prendre en compte la nouvelle config
tfsService.reset();
revalidatePath('/settings/integrations');
return {
success: true,
message: 'Configuration TFS sauvegardée avec succès',
};
} catch (error) {
console.error('Erreur sauvegarde config TFS:', error);
return {
success: false,
error:
error instanceof Error ? error.message : 'Erreur lors de la sauvegarde',
};
}
}
/**
* Récupère la configuration TFS
*/
export async function getTfsConfig() {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
const config = await userPreferencesService.getTfsConfig(session.user.id);
return { success: true, data: config };
} catch (error) {
console.error('Erreur récupération config TFS:', error);
return {
success: false,
error:
error instanceof Error
? error.message
: 'Erreur lors de la récupération',
data: {
enabled: false,
organizationUrl: '',
projectName: '',
personalAccessToken: '',
repositories: [],
ignoredRepositories: [],
},
};
}
}
/**
* Sauvegarde les préférences du scheduler TFS
*/
export async function saveTfsSchedulerConfig(
tfsAutoSync: boolean,
tfsSyncInterval: 'hourly' | 'daily' | 'weekly'
) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
await userPreferencesService.saveTfsSchedulerConfig(
session.user.id,
tfsAutoSync,
tfsSyncInterval
);
revalidatePath('/settings/integrations');
return {
success: true,
message: 'Configuration scheduler TFS mise à jour',
};
} catch (error) {
console.error('Erreur sauvegarde scheduler TFS:', error);
return {
success: false,
error:
error instanceof Error
? error.message
: 'Erreur lors de la sauvegarde du scheduler',
};
}
}
/**
* Lance la synchronisation manuelle des Pull Requests TFS
*/
export async function syncTfsPullRequests() {
try {
// Récupérer l'utilisateur connecté
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return {
success: false,
error: 'Utilisateur non authentifié',
};
}
// Lancer la synchronisation via le service singleton
const result = await tfsService.syncTasks(session.user.id);
if (result.success) {
revalidatePath('/');
revalidatePath('/settings/integrations');
return {
success: true,
message: `Synchronisation terminée: ${result.pullRequestsCreated} créées, ${result.pullRequestsUpdated} mises à jour, ${result.pullRequestsDeleted} supprimées`,
data: result,
};
} else {
return {
success: false,
error: result.errors.join(', ') || 'Erreur lors de la synchronisation',
};
}
} catch (error) {
console.error('Erreur sync TFS:', error);
return {
success: false,
error:
error instanceof Error
? error.message
: 'Erreur de connexion lors de la synchronisation',
};
}
}
/**
* Supprime toutes les tâches TFS de la base de données locale
*/
export async function deleteAllTfsTasks() {
try {
// Supprimer toutes les tâches TFS via le service singleton
const result = await tfsService.deleteAllTasks();
if (result.success) {
revalidatePath('/');
revalidatePath('/settings/integrations');
return {
success: true,
message: `${result.deletedCount} tâche(s) TFS supprimée(s) avec succès`,
data: { deletedCount: result.deletedCount },
};
} else {
return {
success: false,
error: result.error || 'Erreur lors de la suppression',
};
}
} catch (error) {
console.error('Erreur suppression TFS:', error);
return {
success: false,
error:
error instanceof Error
? error.message
: 'Erreur de connexion lors de la suppression',
};
}
}

View File

@@ -0,0 +1,6 @@
import NextAuth from 'next-auth';
import { authOptions } from '@/lib/auth';
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };

View File

@@ -0,0 +1,58 @@
import { NextRequest, NextResponse } from 'next/server';
import { usersService } from '@/services/users';
export async function POST(request: NextRequest) {
try {
const { email, name, firstName, lastName, password } = await request.json();
// Validation
if (!email || !password) {
return NextResponse.json(
{ error: 'Email et mot de passe requis' },
{ status: 400 }
);
}
if (password.length < 6) {
return NextResponse.json(
{ error: 'Le mot de passe doit contenir au moins 6 caractères' },
{ status: 400 }
);
}
// Vérifier si l'email existe déjà
const emailExists = await usersService.emailExists(email);
if (emailExists) {
return NextResponse.json(
{ error: 'Un compte avec cet email existe déjà' },
{ status: 400 }
);
}
// Créer l'utilisateur
const user = await usersService.createUser({
email,
name,
firstName,
lastName,
password,
});
return NextResponse.json({
message: 'Compte créé avec succès',
user: {
id: user.id,
email: user.email,
name: user.name,
firstName: user.firstName,
lastName: user.lastName,
},
});
} catch (error) {
console.error('Registration error:', error);
return NextResponse.json(
{ error: 'Erreur lors de la création du compte' },
{ status: 500 }
);
}
}

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { backupService } from '@/services/backup'; import { backupService } from '@/services/data-management/backup';
interface RouteParams { interface RouteParams {
params: Promise<{ params: Promise<{
@@ -7,16 +7,15 @@ interface RouteParams {
}>; }>;
} }
export async function DELETE( export async function DELETE(request: NextRequest, { params }: RouteParams) {
request: NextRequest,
{ params }: RouteParams
) {
try { try {
const { filename } = await params; const { filename } = await params;
// Vérification de sécurité - s'assurer que c'est bien un fichier de backup // Vérification de sécurité - s'assurer que c'est bien un fichier de backup
if (!filename.startsWith('towercontrol_') || if (
(!filename.endsWith('.db') && !filename.endsWith('.db.gz'))) { !filename.startsWith('towercontrol_') ||
(!filename.endsWith('.db') && !filename.endsWith('.db.gz'))
) {
return NextResponse.json( return NextResponse.json(
{ success: false, error: 'Invalid backup filename' }, { success: false, error: 'Invalid backup filename' },
{ status: 400 } { status: 400 }
@@ -27,24 +26,22 @@ export async function DELETE(
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
message: `Backup ${filename} deleted successfully` message: `Backup ${filename} deleted successfully`,
}); });
} catch (error) { } catch (error) {
console.error('Error deleting backup:', error); console.error('Error deleting backup:', error);
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
error: error instanceof Error ? error.message : 'Failed to delete backup' error:
error instanceof Error ? error.message : 'Failed to delete backup',
}, },
{ status: 500 } { status: 500 }
); );
} }
} }
export async function POST( export async function POST(request: NextRequest, { params }: RouteParams) {
request: NextRequest,
{ params }: RouteParams
) {
try { try {
const { filename } = await params; const { filename } = await params;
const body = await request.json(); const body = await request.json();
@@ -52,8 +49,10 @@ export async function POST(
if (action === 'restore') { if (action === 'restore') {
// Vérification de sécurité // Vérification de sécurité
if (!filename.startsWith('towercontrol_') || if (
(!filename.endsWith('.db') && !filename.endsWith('.db.gz'))) { !filename.startsWith('towercontrol_') ||
(!filename.endsWith('.db') && !filename.endsWith('.db.gz'))
) {
return NextResponse.json( return NextResponse.json(
{ success: false, error: 'Invalid backup filename' }, { success: false, error: 'Invalid backup filename' },
{ status: 400 } { status: 400 }
@@ -63,7 +62,10 @@ export async function POST(
// Protection environnement de production // Protection environnement de production
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
return NextResponse.json( return NextResponse.json(
{ success: false, error: 'Restore not allowed in production via API' }, {
success: false,
error: 'Restore not allowed in production via API',
},
{ status: 403 } { status: 403 }
); );
} }
@@ -72,7 +74,7 @@ export async function POST(
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
message: `Database restored from ${filename}` message: `Database restored from ${filename}`,
}); });
} }
@@ -85,10 +87,9 @@ export async function POST(
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
error: error instanceof Error ? error.message : 'Operation failed' error: error instanceof Error ? error.message : 'Operation failed',
}, },
{ status: 500 } { status: 500 }
); );
} }
} }

View File

@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { backupService } from '@/services/backup'; import { backupService } from '@/services/data-management/backup';
import { backupScheduler } from '@/services/backup-scheduler'; import { backupScheduler } from '@/services/data-management/backup-scheduler';
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
@@ -13,14 +13,24 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
data: { logs } data: { logs },
});
}
if (action === 'stats') {
const days = parseInt(searchParams.get('days') || '30');
const stats = await backupService.getBackupStats(days);
return NextResponse.json({
success: true,
data: stats,
}); });
} }
console.log('🔄 API GET /api/backups called'); console.log('🔄 API GET /api/backups called');
// Test de la configuration d'abord // Test de la configuration d'abord
const config = backupService.getConfig(); const config = await backupService.getConfig();
console.log('✅ Config loaded:', config); console.log('✅ Config loaded:', config);
// Test du scheduler // Test du scheduler
@@ -37,20 +47,24 @@ export async function GET(request: NextRequest) {
backups, backups,
scheduler: schedulerStatus, scheduler: schedulerStatus,
config, config,
} },
}; };
console.log('✅ API response ready'); console.log('✅ API response ready');
return NextResponse.json(response); return NextResponse.json(response);
} catch (error) { } catch (error) {
console.error('❌ Error fetching backups:', error); console.error('❌ Error fetching backups:', error);
console.error('Error stack:', error instanceof Error ? error.stack : 'Unknown'); console.error(
'Error stack:',
error instanceof Error ? error.stack : 'Unknown'
);
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
error: error instanceof Error ? error.message : 'Failed to fetch backups', error:
details: error instanceof Error ? error.stack : undefined error instanceof Error ? error.message : 'Failed to fetch backups',
details: error instanceof Error ? error.stack : undefined,
}, },
{ status: 500 } { status: 500 }
); );
@@ -71,7 +85,8 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
skipped: true, skipped: true,
message: 'No changes detected since last backup. Use force=true to create anyway.' message:
'No changes detected since last backup. Use force=true to create anyway.',
}); });
} }
@@ -81,19 +96,22 @@ export async function POST(request: NextRequest) {
await backupService.verifyDatabaseHealth(); await backupService.verifyDatabaseHealth();
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
message: 'Database health check passed' message: 'Database health check passed',
}); });
case 'config': case 'config':
await backupService.updateConfig(params.config); await backupService.updateConfig(params.config);
// Redémarrer le scheduler si la config a changé // Redémarrer le scheduler si la config a changé
if (params.config.enabled !== undefined || params.config.interval !== undefined) { if (
params.config.enabled !== undefined ||
params.config.interval !== undefined
) {
backupScheduler.restart(); backupScheduler.restart();
} }
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
message: 'Configuration updated', message: 'Configuration updated',
data: backupService.getConfig() data: await backupService.getConfig(),
}); });
case 'scheduler': case 'scheduler':
@@ -104,7 +122,7 @@ export async function POST(request: NextRequest) {
} }
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
data: backupScheduler.getStatus() data: backupScheduler.getStatus(),
}); });
default: default:
@@ -118,7 +136,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
error: error instanceof Error ? error.message : 'Unknown error' error: error instanceof Error ? error.message : 'Unknown error',
}, },
{ status: 500 } { status: 500 }
); );

View File

@@ -0,0 +1,28 @@
import { NextRequest, NextResponse } from 'next/server';
import { dailyService } from '@/services/task-management/daily';
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id: checkboxId } = await params;
if (!checkboxId) {
return NextResponse.json(
{ error: 'Checkbox ID is required' },
{ status: 400 }
);
}
const archivedCheckbox = await dailyService.archiveCheckbox(checkboxId);
return NextResponse.json(archivedCheckbox);
} catch (error) {
console.error('Error archiving checkbox:', error);
return NextResponse.json(
{ error: 'Failed to archive checkbox' },
{ status: 500 }
);
}
}

View File

@@ -1,5 +1,7 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { dailyService } from '@/services/daily'; import { dailyService } from '@/services/task-management/daily';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/lib/auth';
/** /**
* API route pour récupérer toutes les dates avec des dailies * API route pour récupérer toutes les dates avec des dailies
@@ -7,9 +9,13 @@ import { dailyService } from '@/services/daily';
*/ */
export async function GET() { export async function GET() {
try { try {
const dates = await dailyService.getDailyDates(); const session = await getServerSession(authOptions);
return NextResponse.json({ dates }); if (!session?.user?.id) {
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
}
const dates = await dailyService.getDailyDates(session.user.id);
return NextResponse.json({ dates });
} catch (error) { } catch (error) {
console.error('Erreur lors de la récupération des dates:', error); console.error('Erreur lors de la récupération des dates:', error);
return NextResponse.json( return NextResponse.json(

View File

@@ -0,0 +1,42 @@
import { NextResponse } from 'next/server';
import { dailyService } from '@/services/task-management/daily';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/lib/auth';
import { parseDate, isValidAPIDate } from '@/lib/date-utils';
/**
* API route pour récupérer les tâches avec deadline pour une date donnée
* GET /api/daily/deadline-tasks?date=YYYY-MM-DD
*/
export async function GET(request: Request) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const dateStr = searchParams.get('date');
if (!dateStr || !isValidAPIDate(dateStr)) {
return NextResponse.json(
{ error: 'Date invalide. Format attendu: YYYY-MM-DD' },
{ status: 400 }
);
}
const date = parseDate(dateStr);
const tasks = await dailyService.getTasksByDeadlineDate(
session.user.id,
date
);
return NextResponse.json({ tasks });
} catch (error) {
console.error('Erreur lors de la récupération des tâches:', error);
return NextResponse.json(
{ error: 'Erreur interne du serveur' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,30 @@
import { NextResponse } from 'next/server';
import { dailyService } from '@/services/task-management/daily';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/lib/auth';
/**
* API route pour récupérer toutes les dates de fin des tâches avec leurs noms
* GET /api/daily/deadlines
* Retourne un objet { dates: Record<string, string[]> } où chaque clé est une date (YYYY-MM-DD)
* et la valeur est un tableau de noms de tâches
*/
export async function GET() {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
}
const deadlineDates = await dailyService.getTaskDeadlineDates(
session.user.id
);
return NextResponse.json({ dates: deadlineDates });
} catch (error) {
console.error('Erreur lors de la récupération des dates de fin:', error);
return NextResponse.json(
{ error: 'Erreur interne du serveur' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,42 @@
import { NextRequest, NextResponse } from 'next/server';
import { dailyService } from '@/services/task-management/daily';
import { DailyCheckboxType } from '@/lib/types';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/lib/auth';
export async function GET(request: NextRequest) {
try {
// Vérifier l'authentification
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const maxDays = searchParams.get('maxDays')
? parseInt(searchParams.get('maxDays')!)
: undefined;
const excludeToday = searchParams.get('excludeToday') === 'true';
const type = searchParams.get('type') as DailyCheckboxType | undefined;
const limit = searchParams.get('limit')
? parseInt(searchParams.get('limit')!)
: undefined;
const pendingCheckboxes = await dailyService.getPendingCheckboxes({
maxDays,
excludeToday,
type,
limit,
userId: session.user.id, // Filtrer par user connecté
});
return NextResponse.json(pendingCheckboxes);
} catch (error) {
console.error('Error fetching pending checkboxes:', error);
return NextResponse.json(
{ error: 'Failed to fetch pending checkboxes' },
{ status: 500 }
);
}
}

View File

@@ -1,12 +1,24 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { dailyService } from '@/services/daily'; import { dailyService } from '@/services/task-management/daily';
import { getToday, parseDate, isValidAPIDate, createDateFromParts } from '@/lib/date-utils'; import {
getToday,
parseDate,
isValidAPIDate,
createDateFromParts,
} from '@/lib/date-utils';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/lib/auth';
/** /**
* API route pour récupérer la vue daily (hier + aujourd'hui) * API route pour récupérer la vue daily (hier + aujourd'hui)
*/ */
export async function GET(request: Request) { export async function GET(request: Request) {
try { try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
}
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
const action = searchParams.get('action'); const action = searchParams.get('action');
@@ -15,7 +27,10 @@ export async function GET(request: Request) {
if (action === 'history') { if (action === 'history') {
// Récupérer l'historique // Récupérer l'historique
const limit = parseInt(searchParams.get('limit') || '30'); const limit = parseInt(searchParams.get('limit') || '30');
const history = await dailyService.getCheckboxHistory(limit); const history = await dailyService.getCheckboxHistory(
session.user.id,
limit
);
return NextResponse.json(history); return NextResponse.json(history);
} }
@@ -25,7 +40,10 @@ export async function GET(request: Request) {
const limit = parseInt(searchParams.get('limit') || '20'); const limit = parseInt(searchParams.get('limit') || '20');
if (!query.trim()) { if (!query.trim()) {
return NextResponse.json({ error: 'Query parameter required' }, { status: 400 }); return NextResponse.json(
{ error: 'Query parameter required' },
{ status: 400 }
);
} }
const checkboxes = await dailyService.searchCheckboxes(query, limit); const checkboxes = await dailyService.searchCheckboxes(query, limit);
@@ -47,9 +65,11 @@ export async function GET(request: Request) {
targetDate = getToday(); targetDate = getToday();
} }
const dailyView = await dailyService.getDailyView(targetDate); const dailyView = await dailyService.getDailyView(
targetDate,
session.user.id
);
return NextResponse.json(dailyView); return NextResponse.json(dailyView);
} catch (error) { } catch (error) {
console.error('Erreur lors de la récupération du daily:', error); console.error('Erreur lors de la récupération du daily:', error);
return NextResponse.json( return NextResponse.json(
@@ -64,6 +84,10 @@ export async function GET(request: Request) {
*/ */
export async function POST(request: Request) { export async function POST(request: Request) {
try { try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
}
const body = await request.json(); const body = await request.json();
// Validation des données // Validation des données
@@ -93,17 +117,17 @@ export async function POST(request: Request) {
const checkbox = await dailyService.addCheckbox({ const checkbox = await dailyService.addCheckbox({
date, date,
userId: session.user.id,
text: body.text, text: body.text,
type: body.type, type: body.type,
taskId: body.taskId, taskId: body.taskId,
order: body.order, order: body.order,
isChecked: body.isChecked isChecked: body.isChecked,
}); });
return NextResponse.json(checkbox, { status: 201 }); return NextResponse.json(checkbox, { status: 201 });
} catch (error) { } catch (error) {
console.error('Erreur lors de l\'ajout de la checkbox:', error); console.error("Erreur lors de l'ajout de la checkbox:", error);
return NextResponse.json( return NextResponse.json(
{ error: 'Erreur interne du serveur' }, { error: 'Erreur interne du serveur' },
{ status: 500 } { status: 500 }

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/services/database'; import { prisma } from '@/services/core/database';
/** /**
* Route GET /api/jira/logs * Route GET /api/jira/logs
@@ -12,25 +12,24 @@ export async function GET(request: NextRequest) {
const logs = await prisma.syncLog.findMany({ const logs = await prisma.syncLog.findMany({
where: { where: {
source: 'jira' source: 'jira',
}, },
orderBy: { orderBy: {
createdAt: 'desc' createdAt: 'desc',
}, },
take: limit take: limit,
}); });
return NextResponse.json({ return NextResponse.json({
data: logs data: logs,
}); });
} catch (error) { } catch (error) {
console.error('❌ Erreur récupération logs Jira:', error); console.error('❌ Erreur récupération logs Jira:', error);
return NextResponse.json( return NextResponse.json(
{ {
error: 'Erreur lors de la récupération des logs', error: 'Erreur lors de la récupération des logs',
details: error instanceof Error ? error.message : 'Erreur inconnue' details: error instanceof Error ? error.message : 'Erreur inconnue',
}, },
{ status: 500 } { status: 500 }
); );

View File

@@ -1,7 +1,12 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { createJiraService, JiraService } from '@/services/jira'; import {
import { userPreferencesService } from '@/services/user-preferences'; createJiraService,
import { jiraScheduler } from '@/services/jira-scheduler'; JiraService,
} from '@/services/integrations/jira/jira';
import { userPreferencesService } from '@/services/core/user-preferences';
import { jiraScheduler } from '@/services/integrations/jira/scheduler';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
/** /**
* Route POST /api/jira/sync * Route POST /api/jira/sync
@@ -10,6 +15,14 @@ import { jiraScheduler } from '@/services/jira-scheduler';
*/ */
export async function POST(request: Request) { export async function POST(request: Request) {
try { try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: 'Non authentifié' },
{ status: 401 }
);
}
// Vérifier s'il y a des actions spécifiques (scheduler) // Vérifier s'il y a des actions spécifiques (scheduler)
const body = await request.json().catch(() => ({})); const body = await request.json().catch(() => ({}));
const { action, ...params } = body; const { action, ...params } = body;
@@ -19,26 +32,27 @@ export async function POST(request: Request) {
switch (action) { switch (action) {
case 'scheduler': case 'scheduler':
if (params.enabled) { if (params.enabled) {
await jiraScheduler.start(); await jiraScheduler.start(session.user.id);
} else { } else {
jiraScheduler.stop(); jiraScheduler.stop();
} }
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
data: await jiraScheduler.getStatus() data: await jiraScheduler.getStatus(session.user.id),
}); });
case 'config': case 'config':
await userPreferencesService.saveJiraSchedulerConfig( await userPreferencesService.saveJiraSchedulerConfig(
session.user.id,
params.jiraAutoSync, params.jiraAutoSync,
params.jiraSyncInterval params.jiraSyncInterval
); );
// Redémarrer le scheduler si la config a changé // Redémarrer le scheduler si la config a changé
await jiraScheduler.restart(); await jiraScheduler.restart(session.user.id);
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
message: 'Configuration scheduler mise à jour', message: 'Configuration scheduler mise à jour',
data: await jiraScheduler.getStatus() data: await jiraScheduler.getStatus(session.user.id),
}); });
default: default:
@@ -50,18 +64,26 @@ export async function POST(request: Request) {
} }
// Synchronisation normale (manuelle) // Synchronisation normale (manuelle)
const jiraConfig = await userPreferencesService.getJiraConfig(); const jiraConfig = await userPreferencesService.getJiraConfig(
session.user.id
);
let jiraService: JiraService | null = null; let jiraService: JiraService | null = null;
if (jiraConfig.enabled && jiraConfig.baseUrl && jiraConfig.email && jiraConfig.apiToken) { if (
jiraConfig.enabled &&
jiraConfig.baseUrl &&
jiraConfig.email &&
jiraConfig.apiToken
) {
// Utiliser la config depuis la base de données // Utiliser la config depuis la base de données
jiraService = new JiraService({ jiraService = new JiraService({
enabled: jiraConfig.enabled,
baseUrl: jiraConfig.baseUrl, baseUrl: jiraConfig.baseUrl,
email: jiraConfig.email, email: jiraConfig.email,
apiToken: jiraConfig.apiToken, apiToken: jiraConfig.apiToken,
projectKey: jiraConfig.projectKey, projectKey: jiraConfig.projectKey,
ignoredProjects: jiraConfig.ignoredProjects || [] ignoredProjects: jiraConfig.ignoredProjects || [],
}); });
} else { } else {
// Fallback sur les variables d'environnement // Fallback sur les variables d'environnement
@@ -70,7 +92,10 @@ export async function POST(request: Request) {
if (!jiraService) { if (!jiraService) {
return NextResponse.json( return NextResponse.json(
{ error: 'Configuration Jira manquante. Configurez Jira dans les paramètres ou vérifiez les variables d\'environnement.' }, {
error:
"Configuration Jira manquante. Configurez Jira dans les paramètres ou vérifiez les variables d'environnement.",
},
{ status: 400 } { status: 400 }
); );
} }
@@ -81,36 +106,62 @@ export async function POST(request: Request) {
const connectionOk = await jiraService.testConnection(); const connectionOk = await jiraService.testConnection();
if (!connectionOk) { if (!connectionOk) {
return NextResponse.json( return NextResponse.json(
{ error: 'Impossible de se connecter à Jira. Vérifiez la configuration.' }, {
error:
'Impossible de se connecter à Jira. Vérifiez la configuration.',
},
{ status: 401 } { status: 401 }
); );
} }
// Effectuer la synchronisation // Effectuer la synchronisation
const result = await jiraService.syncTasks(); const syncResult = await jiraService.syncTasks(session.user.id);
if (result.success) { // Convertir SyncResult en JiraSyncResult pour le client
// Utiliser les actions Jira originales si disponibles pour préserver les détails (changes, etc.)
const actions =
syncResult.jiraActions ||
syncResult.actions.map((action) => ({
type: action.type as 'created' | 'updated' | 'skipped' | 'deleted',
taskKey: action.itemId.toString(),
taskTitle: action.title,
reason: action.message,
changes: action.message ? [action.message] : undefined,
}));
const jiraSyncResult = {
success: syncResult.success,
tasksFound: syncResult.totalItems,
tasksCreated: syncResult.stats.created,
tasksUpdated: syncResult.stats.updated,
tasksSkipped: syncResult.stats.skipped,
tasksDeleted: syncResult.stats.deleted,
errors: syncResult.errors,
unknownStatuses: syncResult.unknownStatuses || [], // Nouveaux statuts inconnus
actions,
};
if (syncResult.success) {
return NextResponse.json({ return NextResponse.json({
message: 'Synchronisation Jira terminée avec succès', message: 'Synchronisation Jira terminée avec succès',
data: result data: jiraSyncResult,
}); });
} else { } else {
return NextResponse.json( return NextResponse.json(
{ {
error: 'Synchronisation Jira terminée avec des erreurs', error: 'Synchronisation Jira terminée avec des erreurs',
data: result data: jiraSyncResult,
}, },
{ status: 207 } // Multi-Status { status: 207 } // Multi-Status
); );
} }
} catch (error) { } catch (error) {
console.error('❌ Erreur API sync Jira:', error); console.error('❌ Erreur API sync Jira:', error);
return NextResponse.json( return NextResponse.json(
{ {
error: 'Erreur interne lors de la synchronisation', error: 'Erreur interne lors de la synchronisation',
details: error instanceof Error ? error.message : 'Erreur inconnue' details: error instanceof Error ? error.message : 'Erreur inconnue',
}, },
{ status: 500 } { status: 500 }
); );
@@ -123,19 +174,35 @@ export async function POST(request: Request) {
*/ */
export async function GET() { export async function GET() {
try { try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: 'Non authentifié' },
{ status: 401 }
);
}
// Essayer d'abord la config depuis la base de données // Essayer d'abord la config depuis la base de données
const jiraConfig = await userPreferencesService.getJiraConfig(); const jiraConfig = await userPreferencesService.getJiraConfig(
session.user.id
);
let jiraService: JiraService | null = null; let jiraService: JiraService | null = null;
if (jiraConfig.enabled && jiraConfig.baseUrl && jiraConfig.email && jiraConfig.apiToken) { if (
jiraConfig.enabled &&
jiraConfig.baseUrl &&
jiraConfig.email &&
jiraConfig.apiToken
) {
// Utiliser la config depuis la base de données // Utiliser la config depuis la base de données
jiraService = new JiraService({ jiraService = new JiraService({
enabled: jiraConfig.enabled,
baseUrl: jiraConfig.baseUrl, baseUrl: jiraConfig.baseUrl,
email: jiraConfig.email, email: jiraConfig.email,
apiToken: jiraConfig.apiToken, apiToken: jiraConfig.apiToken,
projectKey: jiraConfig.projectKey, projectKey: jiraConfig.projectKey,
ignoredProjects: jiraConfig.ignoredProjects || [] ignoredProjects: jiraConfig.ignoredProjects || [],
}); });
} else { } else {
// Fallback sur les variables d'environnement // Fallback sur les variables d'environnement
@@ -143,12 +210,10 @@ export async function GET() {
} }
if (!jiraService) { if (!jiraService) {
return NextResponse.json( return NextResponse.json({
{
connected: false, connected: false,
message: 'Configuration Jira manquante' message: 'Configuration Jira manquante',
} });
);
} }
const connected = await jiraService.testConnection(); const connected = await jiraService.testConnection();
@@ -156,33 +221,36 @@ export async function GET() {
// Si connexion OK et qu'un projet est configuré, tester aussi le projet // Si connexion OK et qu'un projet est configuré, tester aussi le projet
let projectValidation = null; let projectValidation = null;
if (connected && jiraConfig.projectKey) { if (connected && jiraConfig.projectKey) {
projectValidation = await jiraService.validateProject(jiraConfig.projectKey); projectValidation = await jiraService.validateProject(
jiraConfig.projectKey
);
} }
// Récupérer aussi le statut du scheduler // Récupérer aussi le statut du scheduler avec l'utilisateur connecté
const schedulerStatus = await jiraScheduler.getStatus(); const schedulerStatus = await jiraScheduler.getStatus(session.user.id);
return NextResponse.json({ return NextResponse.json({
connected, connected,
message: connected ? 'Connexion Jira OK' : 'Impossible de se connecter à Jira', message: connected
project: projectValidation ? { ? 'Connexion Jira OK'
: 'Impossible de se connecter à Jira',
project: projectValidation
? {
key: jiraConfig.projectKey, key: jiraConfig.projectKey,
exists: projectValidation.exists, exists: projectValidation.exists,
name: projectValidation.name, name: projectValidation.name,
error: projectValidation.error error: projectValidation.error,
} : null, }
scheduler: schedulerStatus : null,
scheduler: schedulerStatus,
}); });
} catch (error) { } catch (error) {
console.error('❌ Erreur test connexion Jira:', error); console.error('❌ Erreur test connexion Jira:', error);
return NextResponse.json( return NextResponse.json({
{
connected: false, connected: false,
message: 'Erreur lors du test de connexion', message: 'Erreur lors du test de connexion',
details: error instanceof Error ? error.message : 'Erreur inconnue' details: error instanceof Error ? error.message : 'Erreur inconnue',
} });
);
} }
} }

View File

@@ -1,6 +1,8 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { createJiraService } from '@/services/jira'; import { createJiraService } from '@/services/integrations/jira/jira';
import { userPreferencesService } from '@/services/user-preferences'; import { userPreferencesService } from '@/services/core/user-preferences';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
/** /**
* POST /api/jira/validate-project * POST /api/jira/validate-project
@@ -8,6 +10,11 @@ import { userPreferencesService } from '@/services/user-preferences';
*/ */
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
}
const body = await request.json(); const body = await request.json();
const { projectKey } = body; const { projectKey } = body;
@@ -19,11 +26,21 @@ export async function POST(request: NextRequest) {
} }
// Récupérer la config Jira depuis la base de données // Récupérer la config Jira depuis la base de données
const jiraConfig = await userPreferencesService.getJiraConfig(); const jiraConfig = await userPreferencesService.getJiraConfig(
session.user.id
);
if (!jiraConfig.enabled || !jiraConfig.baseUrl || !jiraConfig.email || !jiraConfig.apiToken) { if (
!jiraConfig.enabled ||
!jiraConfig.baseUrl ||
!jiraConfig.email ||
!jiraConfig.apiToken
) {
return NextResponse.json( return NextResponse.json(
{ error: 'Configuration Jira manquante. Configurez Jira dans les paramètres.' }, {
error:
'Configuration Jira manquante. Configurez Jira dans les paramètres.',
},
{ status: 400 } { status: 400 }
); );
} }
@@ -32,37 +49,44 @@ export async function POST(request: NextRequest) {
const jiraService = createJiraService(); const jiraService = createJiraService();
if (!jiraService) { if (!jiraService) {
return NextResponse.json( return NextResponse.json(
{ error: 'Impossible de créer le service Jira. Vérifiez la configuration.' }, {
error:
'Impossible de créer le service Jira. Vérifiez la configuration.',
},
{ status: 500 } { status: 500 }
); );
} }
// Valider le projet // Valider le projet
const validation = await jiraService.validateProject(projectKey.trim().toUpperCase()); const validation = await jiraService.validateProject(
projectKey.trim().toUpperCase()
);
if (validation.exists) { if (validation.exists) {
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
exists: true, exists: true,
projectName: validation.name, projectName: validation.name,
message: `Projet "${projectKey}" trouvé : ${validation.name}` message: `Projet "${projectKey}" trouvé : ${validation.name}`,
}); });
} else { } else {
return NextResponse.json({ return NextResponse.json(
{
success: false, success: false,
exists: false, exists: false,
error: validation.error, error: validation.error,
message: validation.error || `Projet "${projectKey}" introuvable` message: validation.error || `Projet "${projectKey}" introuvable`,
}, { status: 404 }); },
{ status: 404 }
);
} }
} catch (error) { } catch (error) {
console.error('Erreur lors de la validation du projet Jira:', error); console.error('Erreur lors de la validation du projet Jira:', error);
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
error: 'Erreur lors de la validation du projet', error: 'Erreur lors de la validation du projet',
message: error instanceof Error ? error.message : 'Erreur inconnue' message: error instanceof Error ? error.message : 'Erreur inconnue',
}, },
{ status: 500 } { status: 500 }
); );

View File

@@ -0,0 +1,116 @@
import { NextResponse } from 'next/server';
import { notesService } from '@/services/notes';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
/**
* API route pour récupérer une note spécifique
*/
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const resolvedParams = await params;
const note = await notesService.getNoteById(
resolvedParams.id,
session.user.id
);
if (!note) {
return NextResponse.json({ error: 'Note not found' }, { status: 404 });
}
return NextResponse.json({ note });
} catch (error) {
console.error('Error fetching note:', error);
return NextResponse.json(
{ error: 'Failed to fetch note' },
{ status: 500 }
);
}
}
/**
* API route pour mettre à jour une note
*/
export async function PUT(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const { title, content, taskId, tags } = body;
const resolvedParams = await params;
const note = await notesService.updateNote(
resolvedParams.id,
session.user.id,
{
title,
content,
taskId,
tags,
}
);
return NextResponse.json({ note });
} catch (error) {
console.error('Error updating note:', error);
if (
error instanceof Error &&
error.message === 'Note not found or access denied'
) {
return NextResponse.json({ error: 'Note not found' }, { status: 404 });
}
return NextResponse.json(
{ error: 'Failed to update note' },
{ status: 500 }
);
}
}
/**
* API route pour supprimer une note
*/
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const resolvedParams = await params;
await notesService.deleteNote(resolvedParams.id, session.user.id);
return NextResponse.json({ success: true });
} catch (error) {
console.error('Error deleting note:', error);
if (
error instanceof Error &&
error.message === 'Note not found or access denied'
) {
return NextResponse.json({ error: 'Note not found' }, { status: 404 });
}
return NextResponse.json(
{ error: 'Failed to delete note' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,74 @@
import { NextResponse } from 'next/server';
import { notesService } from '@/services/notes';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
/**
* API route pour récupérer toutes les notes de l'utilisateur connecté
*/
export async function GET(request: Request) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const search = searchParams.get('search');
let notes;
if (search) {
notes = await notesService.searchNotes(session.user.id, search);
} else {
notes = await notesService.getNotes(session.user.id);
}
return NextResponse.json({ notes });
} catch (error) {
console.error('Error fetching notes:', error);
return NextResponse.json(
{ error: 'Failed to fetch notes' },
{ status: 500 }
);
}
}
/**
* API route pour créer une nouvelle note
*/
export async function POST(request: Request) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const { title, content, taskId, tags } = body;
if (!title || !content) {
return NextResponse.json(
{ error: 'Title and content are required' },
{ status: 400 }
);
}
const note = await notesService.createNote({
title,
content,
userId: session.user.id,
taskId,
tags,
});
return NextResponse.json({ note }, { status: 201 });
} catch (error) {
console.error('Error creating note:', error);
return NextResponse.json(
{ error: 'Failed to create note' },
{ status: 500 }
);
}
}

View File

@@ -1,5 +1,7 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { tagsService } from '@/services/tags'; import { tagsService } from '@/services/task-management/tags';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/lib/auth';
/** /**
* GET /api/tags/[id] - Récupère un tag par son ID * GET /api/tags/[id] - Récupère un tag par son ID
@@ -9,27 +11,29 @@ export async function GET(
{ params }: { params: Promise<{ id: string }> } { params }: { params: Promise<{ id: string }> }
) { ) {
try { try {
// Vérifier l'authentification
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
}
const { id } = await params; const { id } = await params;
const tag = await tagsService.getTagById(id); const tag = await tagsService.getTagById(id, session.user.id);
if (!tag) { if (!tag) {
return NextResponse.json( return NextResponse.json({ error: 'Tag non trouvé' }, { status: 404 });
{ error: 'Tag non trouvé' },
{ status: 404 }
);
} }
return NextResponse.json({ return NextResponse.json({
data: tag, data: tag,
message: 'Tag récupéré avec succès' message: 'Tag récupéré avec succès',
}); });
} catch (error) { } catch (error) {
console.error('Erreur lors de la récupération du tag:', error); console.error('Erreur lors de la récupération du tag:', error);
return NextResponse.json( return NextResponse.json(
{ {
error: 'Erreur lors de la récupération du tag', error: 'Erreur lors de la récupération du tag',
message: error instanceof Error ? error.message : 'Erreur inconnue' message: error instanceof Error ? error.message : 'Erreur inconnue',
}, },
{ status: 500 } { status: 500 }
); );

View File

@@ -1,11 +1,19 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { tagsService } from '@/services/tags'; import { tagsService } from '@/services/task-management/tags';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/lib/auth';
/** /**
* GET /api/tags - Récupère tous les tags ou recherche par query * GET /api/tags - Récupère tous les tags ou recherche par query
*/ */
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
// Vérifier l'authentification
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
}
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
const query = searchParams.get('q'); const query = searchParams.get('q');
const popular = searchParams.get('popular'); const popular = searchParams.get('popular');
@@ -14,27 +22,26 @@ export async function GET(request: NextRequest) {
let tags; let tags;
if (popular === 'true') { if (popular === 'true') {
// Récupérer les tags les plus utilisés // Récupérer les tags les plus utilisés pour cet utilisateur
tags = await tagsService.getPopularTags(limit); tags = await tagsService.getPopularTags(session.user.id, limit);
} else if (query) { } else if (query) {
// Recherche par nom (pour autocomplete) // Recherche par nom (pour autocomplete) pour cet utilisateur
tags = await tagsService.searchTags(query, limit); tags = await tagsService.searchTags(query, session.user.id, limit);
} else { } else {
// Récupérer tous les tags // Récupérer tous les tags de cet utilisateur
tags = await tagsService.getTags(); tags = await tagsService.getTags(session.user.id);
} }
return NextResponse.json({ return NextResponse.json({
data: tags, data: tags,
message: 'Tags récupérés avec succès' message: 'Tags récupérés avec succès',
}); });
} catch (error) { } catch (error) {
console.error('Erreur lors de la récupération des tags:', error); console.error('Erreur lors de la récupération des tags:', error);
return NextResponse.json( return NextResponse.json(
{ {
error: 'Erreur lors de la récupération des tags', error: 'Erreur lors de la récupération des tags',
message: error instanceof Error ? error.message : 'Erreur inconnue' message: error instanceof Error ? error.message : 'Erreur inconnue',
}, },
{ status: 500 } { status: 500 }
); );

View File

@@ -1,5 +1,7 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { tasksService } from '@/services/tasks'; import { tasksService } from '@/services/task-management/tasks';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
export async function GET( export async function GET(
request: NextRequest, request: NextRequest,
@@ -15,7 +17,16 @@ export async function GET(
); );
} }
const checkboxes = await tasksService.getTaskRelatedCheckboxes(id); // Vérifier l'authentification
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
}
const checkboxes = await tasksService.getTaskRelatedCheckboxes(
session.user.id,
id
);
return NextResponse.json({ data: checkboxes }); return NextResponse.json({ data: checkboxes });
} catch (error) { } catch (error) {

View File

@@ -1,12 +1,23 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { tasksService } from '@/services/tasks'; import { tasksService } from '@/services/task-management/tasks';
import { TaskStatus } from '@/lib/types'; import { TaskStatus } from '@/lib/types';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/lib/auth';
/** /**
* API route pour récupérer les tâches avec filtres optionnels * API route pour récupérer les tâches avec filtres optionnels
*/ */
export async function GET(request: Request) { export async function GET(request: Request) {
try { try {
// Vérifier l'authentification
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: 'Non authentifié' },
{ status: 401 }
);
}
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
// Extraire les paramètres de filtre // Extraire les paramètres de filtre
@@ -16,6 +27,7 @@ export async function GET(request: Request) {
search?: string; search?: string;
limit?: number; limit?: number;
offset?: number; offset?: number;
ownerId?: string; // Filtre par propriétaire
} = {}; } = {};
const status = searchParams.get('status'); const status = searchParams.get('status');
@@ -44,24 +56,26 @@ export async function GET(request: Request) {
} }
// Récupérer les tâches // Récupérer les tâches
const tasks = await tasksService.getTasks(filters); const tasks = await tasksService.getTasks(session.user.id, filters);
const stats = await tasksService.getTaskStats(); const stats = await tasksService.getTaskStats(session.user.id);
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
data: tasks, data: tasks,
stats, stats,
filters: filters, filters: filters,
count: tasks.length count: tasks.length,
}); });
} catch (error) { } catch (error) {
console.error('❌ Erreur lors de la récupération des tâches:', error); console.error('❌ Erreur lors de la récupération des tâches:', error);
return NextResponse.json({ return NextResponse.json(
{
success: false, success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue' error: error instanceof Error ? error.message : 'Erreur inconnue',
}, { status: 500 }); },
{ status: 500 }
);
} }
} }

View File

@@ -0,0 +1,46 @@
import { NextResponse } from 'next/server';
import { tfsService } from '@/services/integrations/tfs/tfs';
/**
* Supprime toutes les tâches TFS de la base de données locale
*/
export async function DELETE() {
try {
console.log('🔄 Début de la suppression des tâches TFS...');
// Supprimer via le service singleton
const result = await tfsService.deleteAllTasks();
if (result.success) {
return NextResponse.json({
success: true,
message:
result.deletedCount > 0
? `${result.deletedCount} tâche(s) TFS supprimée(s) avec succès`
: 'Aucune tâche TFS trouvée à supprimer',
data: {
deletedCount: result.deletedCount,
},
});
} else {
return NextResponse.json(
{
success: false,
error: result.error || 'Erreur lors de la suppression',
},
{ status: 500 }
);
}
} catch (error) {
console.error('❌ Erreur lors de la suppression des tâches TFS:', error);
return NextResponse.json(
{
success: false,
error: 'Erreur lors de la suppression des tâches TFS',
details: error instanceof Error ? error.message : 'Erreur inconnue',
},
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,111 @@
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { userPreferencesService } from '@/services/core/user-preferences';
import { tfsScheduler } from '@/services/integrations/tfs/scheduler';
/**
* GET /api/tfs/scheduler-config
* Récupère la configuration du scheduler TFS
*/
export async function GET() {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: 'Non authentifié' },
{ status: 401 }
);
}
const schedulerConfig = await userPreferencesService.getTfsSchedulerConfig(
session.user.id
);
return NextResponse.json({
success: true,
data: schedulerConfig,
});
} catch (error) {
console.error('Erreur récupération config scheduler TFS:', error);
return NextResponse.json(
{
success: false,
error:
error instanceof Error
? error.message
: 'Erreur lors de la récupération',
},
{ status: 500 }
);
}
}
/**
* POST /api/tfs/scheduler-config
* Sauvegarde la configuration du scheduler TFS
*/
export async function POST(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: 'Non authentifié' },
{ status: 401 }
);
}
const body = await request.json();
const { tfsAutoSync, tfsSyncInterval } = body;
if (typeof tfsAutoSync !== 'boolean') {
return NextResponse.json(
{
success: false,
error: 'tfsAutoSync doit être un booléen',
},
{ status: 400 }
);
}
if (!['hourly', 'daily', 'weekly'].includes(tfsSyncInterval)) {
return NextResponse.json(
{
success: false,
error: 'tfsSyncInterval doit être hourly, daily ou weekly',
},
{ status: 400 }
);
}
await userPreferencesService.saveTfsSchedulerConfig(
session.user.id,
tfsAutoSync,
tfsSyncInterval
);
// Redémarrer le scheduler avec la nouvelle configuration
await tfsScheduler.restart(session.user.id);
// Récupérer le statut mis à jour
const status = await tfsScheduler.getStatus(session.user.id);
return NextResponse.json({
success: true,
message: 'Configuration scheduler TFS mise à jour',
data: status,
});
} catch (error) {
console.error('Erreur sauvegarde config scheduler TFS:', error);
return NextResponse.json(
{
success: false,
error:
error instanceof Error
? error.message
: 'Erreur lors de la sauvegarde',
},
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,39 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { tfsScheduler } from '@/services/integrations/tfs/scheduler';
/**
* GET /api/tfs/scheduler-status
* Récupère le statut du scheduler TFS
*/
export async function GET() {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: 'Non authentifié' },
{ status: 401 }
);
}
const status = await tfsScheduler.getStatus(session.user.id);
return NextResponse.json({
success: true,
data: status,
});
} catch (error) {
console.error('Erreur récupération statut scheduler TFS:', error);
return NextResponse.json(
{
success: false,
error:
error instanceof Error
? error.message
: 'Erreur lors de la récupération',
},
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,96 @@
import { NextResponse } from 'next/server';
import { tfsService } from '@/services/integrations/tfs/tfs';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
/**
* Route POST /api/tfs/sync
* Synchronise les Pull Requests TFS/Azure DevOps avec la base locale
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export async function POST(_request: Request) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: 'Non authentifié' },
{ status: 401 }
);
}
console.log('🔄 Début de la synchronisation TFS manuelle...');
// Effectuer la synchronisation via le service singleton avec l'utilisateur connecté
const result = await tfsService.syncTasks(session.user.id);
if (result.success) {
return NextResponse.json({
message: 'Synchronisation TFS terminée avec succès',
data: result,
});
} else {
return NextResponse.json(
{
error: 'Synchronisation TFS terminée avec des erreurs',
data: result,
},
{ status: 207 } // Multi-Status
);
}
} catch (error) {
console.error('❌ Erreur API sync TFS:', error);
return NextResponse.json(
{
error: 'Erreur interne lors de la synchronisation',
details: error instanceof Error ? error.message : 'Erreur inconnue',
},
{ status: 500 }
);
}
}
/**
* Route GET /api/tfs/sync
* Teste la connexion TFS
*/
export async function GET() {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: 'Non authentifié' },
{ status: 401 }
);
}
// Tester la connexion via le service singleton avec l'utilisateur connecté
const isConnected = await tfsService.testConnection(session.user.id);
if (isConnected) {
return NextResponse.json({
message: 'Connexion TFS OK',
connected: true,
});
} else {
return NextResponse.json(
{
error: 'Connexion TFS échouée',
connected: false,
},
{ status: 401 }
);
}
} catch (error) {
console.error('❌ Erreur test connexion TFS:', error);
return NextResponse.json(
{
error: 'Erreur interne',
details: error instanceof Error ? error.message : 'Erreur inconnue',
connected: false,
},
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,81 @@
import { NextResponse } from 'next/server';
import { tfsService } from '@/services/integrations/tfs/tfs';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
/**
* Route GET /api/tfs/test
* Teste uniquement la connexion TFS/Azure DevOps sans effectuer de synchronisation
*/
export async function GET() {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: 'Non authentifié' },
{ status: 401 }
);
}
console.log('🔄 Test de connexion TFS...');
// Valider la configuration via le service singleton avec l'utilisateur connecté
const configValidation = await tfsService.validateConfig(session.user.id);
if (!configValidation.valid) {
return NextResponse.json(
{
error: 'Configuration TFS invalide',
connected: false,
details: configValidation.error,
},
{ status: 400 }
);
}
// Tester la connexion avec l'utilisateur connecté
const isConnected = await tfsService.testConnection(session.user.id);
if (isConnected) {
// Test approfondi : récupérer des métadonnées
try {
const repositories = await tfsService.getMetadata();
return NextResponse.json({
message: 'Connexion Azure DevOps réussie',
connected: true,
details: {
repositoriesCount: repositories.repositories.length,
},
});
} catch (repoError) {
return NextResponse.json(
{
error: 'Connexion OK mais accès aux repositories limité',
connected: false,
details: `Vérifiez les permissions du token PAT: ${repoError instanceof Error ? repoError.message : 'Erreur inconnue'}`,
},
{ status: 403 }
);
}
} else {
return NextResponse.json(
{
error: 'Connexion Azure DevOps échouée',
connected: false,
details: "Vérifiez l'URL d'organisation et le token PAT",
},
{ status: 401 }
);
}
} catch (error) {
console.error('❌ Erreur test connexion TFS:', error);
return NextResponse.json(
{
error: 'Erreur interne',
connected: false,
details: error instanceof Error ? error.message : 'Erreur inconnue',
},
{ status: 500 }
);
}
}

View File

@@ -1,6 +1,8 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { userPreferencesService } from '@/services/user-preferences'; import { userPreferencesService } from '@/services/core/user-preferences';
import { JiraConfig } from '@/lib/types'; import { JiraConfig } from '@/lib/types';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
/** /**
* GET /api/user-preferences/jira-config * GET /api/user-preferences/jira-config
@@ -8,7 +10,14 @@ import { JiraConfig } from '@/lib/types';
*/ */
export async function GET() { export async function GET() {
try { try {
const jiraConfig = await userPreferencesService.getJiraConfig(); const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
}
const jiraConfig = await userPreferencesService.getJiraConfig(
session.user.id
);
return NextResponse.json({ jiraConfig }); return NextResponse.json({ jiraConfig });
} catch (error) { } catch (error) {
console.error('Erreur lors de la récupération de la config Jira:', error); console.error('Erreur lors de la récupération de la config Jira:', error);
@@ -25,6 +34,11 @@ export async function GET() {
*/ */
export async function PUT(request: NextRequest) { export async function PUT(request: NextRequest) {
try { try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
}
const body = await request.json(); const body = await request.json();
const { baseUrl, email, apiToken, projectKey, ignoredProjects } = body; const { baseUrl, email, apiToken, projectKey, ignoredProjects } = body;
@@ -62,19 +76,21 @@ export async function PUT(request: NextRequest) {
enabled: true, enabled: true,
projectKey: projectKey ? projectKey.trim().toUpperCase() : undefined, projectKey: projectKey ? projectKey.trim().toUpperCase() : undefined,
ignoredProjects: Array.isArray(ignoredProjects) ignoredProjects: Array.isArray(ignoredProjects)
? ignoredProjects.map((p: string) => p.trim().toUpperCase()).filter((p: string) => p.length > 0) ? ignoredProjects
: [] .map((p: string) => p.trim().toUpperCase())
.filter((p: string) => p.length > 0)
: [],
}; };
await userPreferencesService.saveJiraConfig(jiraConfig); await userPreferencesService.saveJiraConfig(session.user.id, jiraConfig);
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
message: 'Configuration Jira sauvegardée avec succès', message: 'Configuration Jira sauvegardée avec succès',
jiraConfig: { jiraConfig: {
...jiraConfig, ...jiraConfig,
apiToken: '••••••••' // Masquer le token dans la réponse apiToken: '••••••••', // Masquer le token dans la réponse
} },
}); });
} catch (error) { } catch (error) {
console.error('Erreur lors de la sauvegarde de la config Jira:', error); console.error('Erreur lors de la sauvegarde de la config Jira:', error);
@@ -91,19 +107,24 @@ export async function PUT(request: NextRequest) {
*/ */
export async function DELETE() { export async function DELETE() {
try { try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
}
const defaultConfig: JiraConfig = { const defaultConfig: JiraConfig = {
baseUrl: '', baseUrl: '',
email: '', email: '',
apiToken: '', apiToken: '',
enabled: false, enabled: false,
ignoredProjects: [] ignoredProjects: [],
}; };
await userPreferencesService.saveJiraConfig(defaultConfig); await userPreferencesService.saveJiraConfig(session.user.id, defaultConfig);
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
message: 'Configuration Jira réinitialisée avec succès' message: 'Configuration Jira réinitialisée avec succès',
}); });
} catch (error) { } catch (error) {
console.error('Erreur lors de la suppression de la config Jira:', error); console.error('Erreur lors de la suppression de la config Jira:', error);

View File

@@ -1,23 +1,35 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { userPreferencesService } from '@/services/user-preferences'; import { userPreferencesService } from '@/services/core/user-preferences';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
/** /**
* GET /api/user-preferences - Récupère toutes les préférences utilisateur * GET /api/user-preferences - Récupère toutes les préférences utilisateur
*/ */
export async function GET() { export async function GET() {
try { try {
const preferences = await userPreferencesService.getAllPreferences(); const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: 'Non authentifié' },
{ status: 401 }
);
}
const preferences = await userPreferencesService.getAllPreferences(
session.user.id
);
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
data: preferences data: preferences,
}); });
} catch (error) { } catch (error) {
console.error('Erreur lors de la récupération des préférences:', error); console.error('Erreur lors de la récupération des préférences:', error);
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
error: 'Erreur lors de la récupération des préférences' error: 'Erreur lors de la récupération des préférences',
}, },
{ status: 500 } { status: 500 }
); );
@@ -29,20 +41,31 @@ export async function GET() {
*/ */
export async function PUT(request: NextRequest) { export async function PUT(request: NextRequest) {
try { try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: 'Non authentifié' },
{ status: 401 }
);
}
const preferences = await request.json(); const preferences = await request.json();
await userPreferencesService.saveAllPreferences(preferences); await userPreferencesService.saveAllPreferences(
session.user.id,
preferences
);
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
message: 'Préférences sauvegardées avec succès' message: 'Préférences sauvegardées avec succès',
}); });
} catch (error) { } catch (error) {
console.error('Erreur lors de la sauvegarde des préférences:', error); console.error('Erreur lors de la sauvegarde des préférences:', error);
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,
error: 'Erreur lors de la sauvegarde des préférences' error: 'Erreur lors de la sauvegarde des préférences',
}, },
{ status: 500 } { status: 500 }
); );

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