163 Commits

Author SHA1 Message Date
Julien Froidefond
040efcdf29 chore: update file structure for uploaded images, moving storage to data/uploads and enhancing README documentation 2026-01-28 11:33:40 +01:00
Julien Froidefond
b969ab2bd8 refactor(NotesPageClient, MarkdownEditor): streamline layout by refining flex properties for better responsiveness 2026-01-28 11:29:32 +01:00
Julien Froidefond
c437baa8e6 refactor(NotesPageClient, MarkdownEditor): enhance layout responsiveness by adjusting flex properties and ensuring proper overflow handling 2026-01-28 11:24:05 +01:00
Julien Froidefond
69f8dcb7bf fix(MarkdownEditor): correct image upload status message to use proper HTML entity for apostrophe 2026-01-12 12:13:56 +01:00
Julien Froidefond
7c777a87fb feat(MarkdownEditor): implement image upload functionality and handle image pasting in Markdown editor 2026-01-12 12:09:42 +01:00
Julien Froidefond
1eef15f502 refactor(Notes): update favorite filtering logic for clarity and consistency in NotesPageClient and NotesList components 2026-01-12 10:59:11 +01:00
Julien Froidefond
1ea76fed64 refactor(FoldersSidebar): reorganize button and notes count display for improved layout and accessibility 2026-01-12 10:59:06 +01:00
Julien Froidefond
75d31e86ac feat(Notes): add favorite functionality to notes, allowing users to toggle favorites and filter notes accordingly 2026-01-12 10:52:44 +01:00
Julien Froidefond
31d01c2926 feat(Notes): add drag-and-drop functionality to SortableNoteItem for improved note reordering 2026-01-06 14:55:54 +01:00
Julien Froidefond
2354a353d1 feat(Notes): implement manual ordering for notes, add drag-and-drop functionality, and update related components for reordering 2026-01-06 14:51:12 +01:00
Julien Froidefond
38ccaf8785 feat(Notes): add folderId to note creation and update processes, enhance folder handling in NotesPageClient 2026-01-06 09:41:33 +01:00
Julien Froidefond
13070790cc refactor(FoldersSidebar): remove unused TagIcon import and related tag variable for cleaner code 2026-01-06 09:24:53 +01:00
Julien Froidefond
6c4c6992a9 feat(Notes): add folder management to notes, allowing notes to be categorized into folders, and update related components for folder selection and display 2026-01-06 09:05:27 +01:00
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
420 changed files with 50685 additions and 19592 deletions

BIN
.DS_Store vendored

Binary file not shown.

6
.gitignore vendored
View File

@@ -45,3 +45,9 @@ next-env.d.ts
/data/*.db
/data/backups/*
/data/uploads/notes/*
!/data/uploads/notes/.gitkeep
# Uploaded images (legacy - now using data/uploads)
/public/uploads/notes/*
!/public/uploads/notes/.gitkeep

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

View File

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

View File

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

157
PNPM_MIGRATION.md Normal file
View File

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

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

View File

@@ -1,6 +1,7 @@
# 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
@@ -8,17 +9,20 @@ Permettre au service TFS de récupérer **toutes** les Pull Requests assignées
### 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
@@ -26,12 +30,14 @@ Permettre au service TFS de récupérer **toutes** les Pull Requests assignées
### 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
- 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
@@ -39,17 +45,20 @@ Permettre au service TFS de récupérer **toutes** les Pull Requests assignées
### 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
@@ -59,6 +68,7 @@ Permettre au service TFS de récupérer **toutes** les Pull Requests assignées
```
### Comportement intelligent :
- **Fusion automatique** des deux types de PRs
- **Déduplication** basée sur `pullRequestId`
- **Filtrage** selon la configuration (repositories, branches, projet)
@@ -74,11 +84,13 @@ Permettre au service TFS de récupérer **toutes** les Pull Requests assignées
## 🎨 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
@@ -94,10 +106,11 @@ Permettre au service TFS de récupérer **toutes** les Pull Requests assigné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.* 🎯
_Cette mise à niveau transforme le service TFS d'un outil de surveillance de projet en un assistant personnel intelligent pour Azure DevOps._ 🎯

81
TODO.md
View File

@@ -1,46 +1,64 @@
# TowerControl v2.0 - Gestionnaire de tâches moderne
## Fix
- [ ] 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
## Idées à developper
- [x] Refacto et intégration design : mode sombre et clair sont souvent mal généré par défaut <!-- Diagnostic terminé -->
- [ ] Personnalisation : couleurs
- [ ] Optimisations Perf : requetes DB
- [ ] PWA et mode offline
---
## 🎨 **REFACTORING THÈME & PERSONNALISATION COULEURS**
## 🐛 Problèmes relevés en réunion - Corrections UI/UX
### **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
### 🎨 Design et Interface
### **Phase 2: Système Couleurs Personnalisées**
- [ ] **Étendre le modèle UserPreferences** pour supporter des couleurs personnalisées
- [ ] **Créer un service de gestion** des couleurs personnalisées
- [ ] **Créer une interface de configuration** des couleurs personnalisées
- [ ] **Implémenter le système CSS** pour les couleurs personnalisées dynamiques
- [ ] **Créer un système de presets** de thèmes (Tech Dark, Corporate Light, etc.)
- [ ] **Ajouter la validation des contrastes** pour les couleurs personnalisées
- [ ] **Permettre export/import** des configurations de thème personnalisées
- [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
### **Problèmes identifiés actuellement :**
- ❌ Approche hybride incohérente (CSS Variables + Tailwind `dark:` + classes conditionnelles)
- ❌ Double application du thème (3 endroits différents)
- ❌ Pas de configuration Tailwind pour `darkMode`
- ❌ Hydration mismatch avec flashs
- ❌ CSS Variables mal optimisées (`:root` contient le thème sombre)
- ❌ Couleurs hardcodées dans certains composants
### 🔧 Fonctionnalités et Intégrations
- [ ] **Synchro Jira et TFS shortcuts** - Ajouter des raccourcis et bouton dans Kanban
- [x] **Intégration suppressions Jira/TFS** - Aligner la gestion des suppressions sur TFS, je veux que ce qu'on a récupéré dans la synchro, quand ca devient terminé dans Jira ou TFS, soit marqué comme terminé dans le Kanban et non supprimé du kanban. <!-- COMPLET: 1) JQL inclut resolved >= -30d pour récupérer tâches terminées, 2) syncSingleTask met à jour status + completedAt, 3) cleanupUnassignedTasks/cleanupInactivePullRequests préservent tâches done/archived -->
- [ ] **Log d'activité** - Implémenter un système de log d'activité (feature potentielle)
---
## 🚀 Nouvelles idées & fonctionnalités futures
### 🎯 Jira - Suivi des demandes en attente
- [ ] **Page "Jiras en attente"**
- [ ] Liste des Jiras créés par moi mais non assignés à mon équipe
- [ ] Suivi des demandes formulées à d'autres équipes
@@ -53,10 +71,12 @@
### 👥 Gestion multi-utilisateurs (PROJET MAJEUR)
#### **Architecture actuelle → Multi-tenant**
- **Problème** : App mono-utilisateur avec données globales
- **Solution** : Transformation en app multi-utilisateurs avec isolation des données + système de rôles
#### **Plan de migration**
- [ ] **Phase 1: Authentification**
- [ ] Système de login/mot de passe (NextAuth.js)
- [ ] Gestion des sessions sécurisées
@@ -139,6 +159,7 @@
- [ ] Historique des modifications par utilisateur
#### **Considérations techniques**
- **Base de données** : Ajouter `userId` partout + contraintes
- **Sécurité** : Validation côté serveur de l'isolation des données
- **Performance** : Index sur `userId`, pagination pour gros volumes
@@ -197,6 +218,7 @@
### **Fonctionnalités IA concrètes**
#### 🎯 **Smart Task Creation**
- [ ] **Bouton "Créer avec IA" dans le Kanban**
- [ ] Input libre : "Préparer présentation client pour vendredi"
- [ ] IA génère : titre, description, estimation durée, sous-tâches
@@ -204,6 +226,7 @@
- [ ] 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
@@ -213,6 +236,7 @@
- [ ] 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
@@ -222,6 +246,7 @@
- [ ] 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 ?"
@@ -232,6 +257,7 @@
- [ ] 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
@@ -242,6 +268,7 @@
- [ ] 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
@@ -255,4 +282,4 @@
---
*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É)
### 1.1 Configuration projet Next.js
- [x] Initialiser Next.js avec TypeScript
- [x] Configurer ESLint, Prettier
- [x] Setup structure de dossiers selon les règles du workspace
@@ -10,12 +11,14 @@
- [x] Setup Prisma ORM
### 1.2 Architecture backend standalone
- [x] Créer `services/database.ts` - Pool de connexion DB
- [x] Créer `services/tasks.ts` - Service CRUD pour les tâches
- [x] Créer `lib/types.ts` - Types partagés (Task, Tag, etc.)
- [x] Nettoyer l'ancien code de synchronisation
### 1.3 API moderne et propre
- [x] `app/api/tasks/route.ts` - API CRUD complète (GET, POST, PATCH, DELETE)
- [x] Supprimer les routes de synchronisation obsolètes
- [x] Configuration moderne dans `lib/config.ts`
@@ -25,12 +28,14 @@
## 🎯 Phase 2: Interface utilisateur moderne (EN COURS)
### 2.1 Système de design et composants UI
- [x] Créer les composants UI de base (Button, Input, Card, Modal, Badge)
- [x] Implémenter le système de design tech dark (couleurs, typographie, spacing)
- [x] Setup Tailwind CSS avec classes utilitaires personnalisées
- [x] Créer une palette de couleurs tech/cyberpunk
### 2.2 Composants Kanban existants (à améliorer)
- [x] `components/kanban/Board.tsx` - Tableau Kanban principal
- [x] `components/kanban/Column.tsx` - Colonnes du Kanban
- [x] `components/kanban/TaskCard.tsx` - Cartes de tâches
@@ -38,6 +43,7 @@
- [x] Refactoriser les composants pour utiliser le nouveau système UI
### 2.3 Gestion des tâches (CRUD)
- [x] Formulaire de création de tâche (Modal + Form)
- [x] Création rapide inline dans les colonnes (QuickAddTask)
- [x] Formulaire d'édition de tâche (Modal + Form avec pré-remplissage)
@@ -47,6 +53,7 @@
- [x] Validation des formulaires et gestion d'erreurs
### 2.4 Gestion des tags
- [x] Créer/éditer des tags avec sélecteur de couleur
- [x] Autocomplete pour les tags existants
- [x] Suppression de tags (avec vérification des dépendances)
@@ -66,6 +73,7 @@
- [x] Intégration des filtres dans KanbanBoard
### 2.5 Clients HTTP et hooks
- [x] `clients/tasks-client.ts` - Client pour les tâches (CRUD complet)
- [x] `clients/tags-client.ts` - Client pour les tags
- [x] `clients/base/http-client.ts` - Client HTTP de base
@@ -76,6 +84,7 @@
- [x] Architecture SSR + hydratation client optimisée
### 2.6 Fonctionnalités Kanban avancées
- [x] Drag & drop entre colonnes (@dnd-kit avec React 19)
- [x] Drag & drop optimiste (mise à jour immédiate + rollback si erreur)
- [x] Filtrage par statut/priorité/assigné
@@ -85,6 +94,7 @@
- [x] Tri des tâches (date, priorité, alphabétique)
### 2.7 Système de thèmes (clair/sombre)
- [x] Créer le contexte de thème (ThemeContext + ThemeProvider)
- [x] Ajouter toggle de thème dans le Header (bouton avec icône soleil/lune)
- [x] Définir les variables CSS pour le thème clair
@@ -99,6 +109,7 @@
## 📊 Phase 3: Intégrations et analytics (Priorité 3)
### 3.1 Gestion du Daily
- [x] Créer `services/daily.ts` - Service de gestion des daily notes
- [x] Modèle de données Daily (date, checkboxes hier/aujourd'hui)
- [x] Interface Daily avec sections "Hier" et "Aujourd'hui"
@@ -111,6 +122,7 @@
- [x] Vue calendar/historique des dailies
### 3.2 Intégration Jira Cloud
- [x] Créer `services/jira.ts` - Service de connexion à l'API Jira Cloud
- [x] Configuration Jira (URL, email, API token) dans `lib/config.ts`
- [x] Authentification Basic Auth (email + API token)
@@ -127,6 +139,7 @@
- [x] Gestion des erreurs et timeouts API
### 3.3 Page d'accueil/dashboard
- [x] Créer une page d'accueil moderne avec vue d'ensemble
- [x] Widgets de statistiques (tâches par statut, priorité, etc.)
- [x] Déplacer kanban vers /kanban et créer nouveau dashboard à la racine
@@ -137,6 +150,7 @@
- [x] Intégration des analytics dans le dashboard
### 3.4 Analytics et métriques
- [x] `services/analytics.ts` - Calculs statistiques
- [x] Métriques de productivité (vélocité, temps moyen, etc.)
- [x] Graphiques avec Recharts (tendances, vélocité, distribution)
@@ -144,6 +158,7 @@
- [x] Insights automatiques et métriques visuelles
## Autre Todo
- [x] Avoir un bouton pour réduire/agrandir la font des taches dans les kanban (swimlane et classique)
- [x] Refactorer les couleurs des priorités dans un seul endroit
- [x] Settings synchro Jira : ajouter une liste de projet à ignorer, doit etre pris en compte par le service bien sur
@@ -161,13 +176,14 @@
- [x] Vérification d'intégrité et restauration sécurisée
- [x] Option de restauration depuis une sauvegarde sélectionnée
## 🔧 Phase 4: Server Actions - Migration API Routes (Nouveau)
### 4.1 Migration vers Server Actions - Actions rapides
**Objectif** : Remplacer les API routes par des server actions pour les actions simples et fréquentes
#### Actions TaskCard (Priorité 1)
- [x] Créer `actions/tasks.ts` avec server actions de base
- [x] `updateTaskStatus(taskId, status)` - Changement de statut
- [x] `updateTaskTitle(taskId, title)` - Édition inline du titre
@@ -181,6 +197,7 @@
- [x] **Nettoyage** : Modifier `useTasks.ts` pour remplacer mutations par server actions
#### Actions Daily (Priorité 2)
- [x] Créer `actions/daily.ts` pour les checkboxes
- [x] `toggleCheckbox(checkboxId)` - Toggle état checkbox
- [x] `addCheckboxToDaily(dailyId, content)` - Ajouter checkbox
@@ -193,6 +210,7 @@
- [x] **Nettoyage** : Modifier hook `useDaily.ts` pour `useTransition`
#### Actions User Preferences (Priorité 3)
- [x] Créer `actions/preferences.ts` pour les toggles
- [x] `updateViewPreferences(preferences)` - Préférences d'affichage
- [x] `updateKanbanFilters(filters)` - Filtres Kanban
@@ -204,6 +222,7 @@
- [x] **Nettoyage** : Modifier `UserPreferencesContext.tsx` pour server actions
#### Actions Tags (Priorité 4)
- [x] Créer `actions/tags.ts` pour la gestion tags
- [x] `createTag(name, color)` - Création tag
- [x] `updateTag(tagId, data)` - Modification tag
@@ -214,29 +233,35 @@
- [x] **Nettoyage** : Modifier `useTags.ts` pour server actions directes
#### Migration progressive avec nettoyage immédiat
**Principe** : Pour chaque action migrée → nettoyage immédiat des routes et code obsolètes
### 4.2 Conservation API Routes - Endpoints complexes
**À GARDER en API routes** (pas de migration)
#### Endpoints de fetching initial
-`GET /api/tasks` - Récupération avec filtres complexes
-`GET /api/daily` - Vue daily avec logique métier
-`GET /api/tags` - Liste tags avec recherche
-`GET /api/user-preferences` - Préférences initiales
#### Endpoints d'intégration externe
-`POST /api/jira/sync` - Synchronisation Jira complexe
-`GET /api/jira/logs` - Logs de synchronisation
- ✅ Configuration Jira (formulaires complexes)
#### Raisons de conservation
- **API publique** : Réutilisable depuis mobile/externe
- **Logique complexe** : Synchronisation, analytics, rapports
- **Monitoring** : Besoin de logs HTTP séparés
- **Real-time futur** : WebSockets/SSE non compatibles server actions
### 4.3 Architecture hybride cible
```
Actions rapides → Server Actions directes
├── TaskCard actions (status, title, delete)
@@ -252,6 +277,7 @@ Endpoints complexes → API Routes conservées
```
### 4.4 Avantages attendus
- **🚀 Performance** : Pas de sérialisation HTTP pour actions rapides
- **🔄 Cache intelligent** : `revalidatePath()` automatique
- **📦 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)
### 5.1 Configuration projet Jira
- [x] Ajouter champ `projectKey` dans la config Jira (settings)
- [x] Interface pour sélectionner le projet à surveiller
- [x] Validation de l'existence du projet via API Jira
@@ -268,6 +295,7 @@ Endpoints complexes → API Routes conservées
- [x] Test de connexion spécifique au projet configuré
### 5.2 Service d'analytics Jira
- [x] Créer `services/jira-analytics.ts` - Métriques avancées
- [x] Récupération des tickets du projet (toute l'équipe, pas seulement assignés)
- [x] Calculs de vélocité d'équipe (story points par sprint)
@@ -278,6 +306,7 @@ Endpoints complexes → API Routes conservées
- [x] Cache intelligent des métriques (éviter API rate limits)
### 5.3 Page de surveillance `/jira-dashboard`
- [x] Créer page dédiée avec navigation depuis settings Jira
- [x] Vue d'ensemble du projet (nom, lead, statut global)
- [x] Sélecteur de période (7j, 30j, 3 mois, sprint actuel)
@@ -287,6 +316,7 @@ Endpoints complexes → API Routes conservées
- [x] Alertes visuelles (tickets en retard, sprints déviants)
### 5.4 Métriques et graphiques avancés
- [x] **Vélocité** : Story points complétés par sprint
- [x] **Burndown chart** : Progression vs planifié
- [x] **Cycle time** : Temps moyen par type de ticket
@@ -297,6 +327,7 @@ Endpoints complexes → API Routes conservées
- [x] **Collaboration** : Matrice d'interactions entre assignees
### 5.5 Fonctionnalités de surveillance
- [x] **Cache serveur intelligent** : Cache en mémoire avec invalidation manuelle
- [x] **Export des métriques** : Export CSV/JSON avec téléchargement automatique
- [x] **Comparaison inter-sprints** : Tendances, prédictions et recommandations
@@ -308,11 +339,13 @@ Endpoints complexes → API Routes conservées
### 📁 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/`
@@ -321,6 +354,7 @@ Endpoints complexes → API Routes conservées
- [x] `mv services/ src/services/`
- [x] **Phase 2: Mise à jour tsconfig.json**
```json
"paths": {
"@/*": ["./src/*"]
@@ -350,6 +384,7 @@ Endpoints complexes → API Routes conservées
- [x] Tester les fonctionnalités principales
#### **Structure finale attendue**
```
src/
├── app/ # Pages Next.js (déjà OK)
@@ -370,18 +405,22 @@ src/
- [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) ✅
@@ -453,8 +492,8 @@ src/services/
```
### 🔄 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)
@@ -466,6 +505,7 @@ src/services/
- [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é
@@ -479,3 +519,77 @@ src/services/
- [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
---

View File

@@ -29,9 +29,7 @@ function TaskCard({ task }) {
return (
<Card>
<CardContent>
<Button variant="primary">
{task.title}
</Button>
<Button variant="primary">{task.title}</Button>
</CardContent>
</Card>
);
@@ -41,6 +39,7 @@ function TaskCard({ task }) {
## 📦 Composants UI Disponibles
### Button
```tsx
<Button variant="primary" size="md">Action</Button>
<Button variant="secondary">Secondaire</Button>
@@ -49,6 +48,7 @@ function TaskCard({ task }) {
```
### Badge
```tsx
<Badge variant="primary">Tag</Badge>
<Badge variant="success">Succès</Badge>
@@ -56,6 +56,7 @@ function TaskCard({ task }) {
```
### Alert
```tsx
<Alert variant="success">
<AlertTitle>Succès</AlertTitle>
@@ -64,29 +65,47 @@ function TaskCard({ task }) {
```
### 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

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

@@ -9,20 +9,25 @@ data/
├── README.md # Ce fichier
├── prod.db # Base de données production (Docker)
├── dev.db # Base de données développement (Docker)
── backups/ # Sauvegardes automatiques et manuelles
├── towercontrol_2025-01-15T10-30-00-000Z.db.gz
├── towercontrol_2025-01-15T11-30-00-000Z.db.gz
── backups/ # Sauvegardes automatiques et manuelles
├── towercontrol_2025-01-15T10-30-00-000Z.db.gz
├── towercontrol_2025-01-15T11-30-00-000Z.db.gz
│ └── ...
└── uploads/ # Images et fichiers uploadés
└── notes/ # Images des notes markdown
└── ...
```
## 🎯 Utilisation
### En développement local
- La base de données principale est dans `prisma/dev.db`
- Ce dossier `data/` est utilisé uniquement par Docker
- Les sauvegardes locales sont dans `backups/` (racine du projet)
### En production Docker
- Base de données : `data/prod.db` ou `data/dev.db`
- Sauvegardes : `data/backups/`
- Tout ce dossier est mappé vers `/app/data` dans le conteneur
@@ -45,31 +50,40 @@ BACKUP_STORAGE_PATH="./data/backups"
## 🗂️ Fichiers
### Bases de données SQLite
- **prod.db** : Base de données de production
- **dev.db** : Base de données de développement Docker
- Format : SQLite 3
- Contient : Tasks, Tags, User Preferences, Sync Logs, etc.
### Sauvegardes
- **Format** : `towercontrol_YYYY-MM-DDTHH-mm-ss-sssZ.db.gz`
- **Compression** : gzip
- **Rétention** : Configurable (défaut: 5 sauvegardes)
- **Fréquence** : Configurable (défaut: horaire)
### Images uploadées
- **Dossier** : `data/uploads/notes/`
- **Format** : Images collées dans les notes markdown
- **Accès** : Via `/api/notes/images/[filename]`
- **Persistance** : Monté en volume Docker pour persistance
## 🚀 Commandes utiles
```bash
# Créer une sauvegarde manuelle
npm run backup:create
pnpm run backup:create
# Lister les sauvegardes
npm run backup:list
pnpm run backup:list
# Voir la configuration
npm run backup:config
pnpm run backup:config
# Restaurer une sauvegarde (dev uniquement)
npm run backup:restore filename.db.gz
pnpm run backup:restore filename.db.gz
```
## ⚠️ Important

0
data/uploads/notes/.gitkeep Executable file
View File

View File

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

View File

@@ -14,6 +14,10 @@ JIRA_BASE_URL="" # https://votre-domaine.atlassian.net
JIRA_EMAIL="" # votre.email@domaine.com
JIRA_API_TOKEN="" # Token API Jira
# NextAuth (requis)
NEXTAUTH_URL="http://localhost:3000" # URL de votre application
NEXTAUTH_SECRET="your-secret-key-here" # Clé secrète pour signer les tokens
# Debug (optionnel)
VERBOSE_LOGGING="false" # Logs détaillés en développement
NODE_ENV="development" # development | production

View File

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

View File

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

7990
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,50 +1,92 @@
{
"name": "towercontrol",
"version": "0.1.0",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build --turbopack",
"build": "prisma generate && next build --turbopack",
"start": "next start",
"postinstall": "prisma generate",
"lint": "eslint",
"backup:create": "npx tsx scripts/backup-manager.ts create",
"backup:list": "npx tsx scripts/backup-manager.ts list",
"backup:verify": "npx tsx scripts/backup-manager.ts verify",
"backup:config": "npx tsx scripts/backup-manager.ts config",
"backup:start": "npx tsx scripts/backup-manager.ts scheduler-start",
"backup:stop": "npx tsx scripts/backup-manager.ts scheduler-stop",
"backup:status": "npx tsx scripts/backup-manager.ts scheduler-status",
"cache:monitor": "npx tsx scripts/cache-monitor.ts",
"cache:stats": "npx tsx scripts/cache-monitor.ts stats",
"cache:cleanup": "npx tsx scripts/cache-monitor.ts cleanup",
"cache:clear": "npx tsx scripts/cache-monitor.ts clear",
"test:story-points": "npx tsx scripts/test-story-points.ts",
"test:jira-fields": "npx tsx scripts/test-jira-fields.ts"
"backup:create": "pnpm tsx scripts/backup-manager.ts create",
"backup:list": "pnpm tsx scripts/backup-manager.ts list",
"backup:verify": "pnpm tsx scripts/backup-manager.ts verify",
"backup:config": "pnpm tsx scripts/backup-manager.ts config",
"backup:start": "pnpm tsx scripts/backup-manager.ts scheduler-start",
"backup:stop": "pnpm tsx scripts/backup-manager.ts scheduler-stop",
"backup:status": "pnpm tsx scripts/backup-manager.ts scheduler-status",
"cache:monitor": "pnpm tsx scripts/cache-monitor.ts",
"cache:stats": "pnpm tsx scripts/cache-monitor.ts stats",
"cache:cleanup": "pnpm tsx scripts/cache-monitor.ts cleanup",
"cache:clear": "pnpm tsx scripts/cache-monitor.ts clear",
"test": "node scripts/test-runner.js",
"test:watch": "vitest --watch --reporter=verbose",
"test:coverage": "vitest --coverage --reporter=verbose",
"test:ui": "vitest --ui",
"test:story-points": "pnpm tsx scripts/test-story-points.ts",
"test:jira-fields": "pnpm tsx scripts/test-jira-fields.ts",
"prettier:format": "prettier --write .",
"prettier:check": "prettier --check .",
"prepare": "husky"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emoji-mart/data": "^1.2.1",
"@emoji-mart/react": "^1.1.1",
"@prisma/client": "^6.16.1",
"bcryptjs": "^3.0.2",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"next": "15.5.3",
"emoji-mart": "^5.6.0",
"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",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-markdown": "^10.1.0",
"recharts": "^3.2.1",
"tailwind-merge": "^3.3.1"
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.1",
"remark-toc": "^9.0.0",
"tailwind-merge": "^3.3.1",
"twemoji": "^14.0.2"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@eslint/eslintrc": "^3.3.3",
"@tailwindcss/postcss": "^4.1.17",
"@types/bcryptjs": "^2.4.6",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "^15.5.3",
"knip": "^5.64.0",
"eslint-config-next": "^15.5.7",
"husky": "^9.1.7",
"knip": "^5.71.0",
"lint-staged": "^15.5.2",
"prettier": "^3.6.2",
"sharp": "^0.34.5",
"tailwindcss": "^4.1.17",
"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 = {
plugins: ["@tailwindcss/postcss"],
plugins: ['@tailwindcss/postcss'],
};
export default config;

0
prisma/data/dev.db Normal file
View File

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

@@ -0,0 +1,23 @@
-- CreateTable
CREATE TABLE "folders" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"tagId" TEXT,
"parentId" TEXT,
"order" INTEGER NOT NULL DEFAULT 0,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "folders_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "folders_tagId_fkey" FOREIGN KEY ("tagId") REFERENCES "tags" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "folders_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "folders" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- AlterTable Note - Add folderId column
ALTER TABLE "Note" ADD COLUMN "folderId" TEXT;
-- CreateIndex
CREATE INDEX "folders_userId_idx" ON "folders"("userId");
CREATE INDEX "folders_parentId_idx" ON "folders"("parentId");

View File

@@ -0,0 +1,3 @@
-- AlterTable Note - Add order column
ALTER TABLE "Note" ADD COLUMN "order" INTEGER NOT NULL DEFAULT 0;

View File

@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Note" ADD COLUMN "isFavorite" BOOLEAN NOT NULL DEFAULT 0;

BIN
prisma/prisma/dev.db Normal file

Binary file not shown.

View File

@@ -7,6 +7,29 @@ datasource db {
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")
folders Folder[] @relation("FolderOwner")
@@map("users")
}
model Task {
id String @id @default(cuid())
title String
@@ -21,15 +44,20 @@ model Task {
updatedAt DateTime @updatedAt
jiraProject String?
jiraKey String?
assignee String?
assignee String? // Legacy field - keep for Jira/TFS compatibility
ownerId String // Required - chaque tâche appartient à un user
owner User @relation("TaskOwner", fields: [ownerId], references: [id], onDelete: Cascade)
jiraType String?
tfsProject String?
tfsPullRequestId Int?
tfsRepository String?
tfsSourceBranch String?
tfsTargetBranch String?
primaryTagId String?
primaryTag Tag? @relation("PrimaryTag", fields: [primaryTagId], references: [id])
dailyCheckboxes DailyCheckbox[]
taskTags TaskTag[]
notes Note[] // Notes associées à cette tâche
@@unique([source, sourceId])
@@map("tasks")
@@ -37,11 +65,17 @@ model Task {
model Tag {
id String @id @default(cuid())
name String @unique
name String
color String @default("#6b7280")
isPinned Boolean @default(false)
ownerId String // Chaque tag appartient à un utilisateur
owner User @relation("TagOwner", fields: [ownerId], references: [id], onDelete: Cascade)
taskTags TaskTag[]
primaryTasks Task[] @relation("PrimaryTag")
noteTags NoteTag[]
folders Folder[]
@@unique([name, ownerId]) // Un nom de tag unique par utilisateur
@@map("tags")
}
@@ -74,16 +108,20 @@ model DailyCheckbox {
type String @default("task")
order Int @default(0)
taskId String?
userId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
task Task? @relation(fields: [taskId], references: [id])
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([date])
@@index([userId])
@@map("daily_checkboxes")
}
model UserPreferences {
id String @id @default(cuid())
userId String @unique
kanbanFilters Json?
viewPreferences Json?
columnVisibility Json?
@@ -95,6 +133,52 @@ model UserPreferences {
tfsSyncInterval String @default("daily")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("user_preferences")
}
model Note {
id String @id @default(cuid())
title String
content String // Markdown content
userId String
taskId String? // Tâche associée à la note
folderId String? // Dossier contenant la note
order Int @default(0) // Ordre manuel de la note dans son dossier
isFavorite Boolean @default(false) // Note favorite
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
task Task? @relation(fields: [taskId], references: [id])
folder Folder? @relation(fields: [folderId], references: [id])
noteTags NoteTag[]
}
model Folder {
id String @id @default(cuid())
name String
userId String
tagId String? // Tag associé au dossier
parentId String? // Dossier parent pour sous-dossiers
order Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation("FolderOwner", fields: [userId], references: [id], onDelete: Cascade)
tag Tag? @relation(fields: [tagId], references: [id])
parent Folder? @relation("FolderHierarchy", fields: [parentId], references: [id], onDelete: Cascade)
children Folder[] @relation("FolderHierarchy")
notes Note[]
@@map("folders")
}
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

View File

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,7 +4,10 @@
* Usage: tsx scripts/backup-manager.ts [command] [options]
*/
import { backupService, BackupConfig } from '../src/services/data-management/backup';
import {
backupService,
BackupConfig,
} from '../src/services/data-management/backup';
import { backupScheduler } from '../src/services/data-management/backup-scheduler';
import { formatDateForDisplay } from '../src/lib/date-utils';
@@ -70,7 +73,10 @@ 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;
// Simulation d'une confirmation (en CLI réel, utiliser readline)
@@ -170,12 +176,16 @@ OPTIONS:
}
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);
if (result === null) {
console.log('⏭️ Sauvegarde sautée: Aucun changement détecté depuis la dernière sauvegarde');
console.log(' 💡 Utilisez --force pour créer une sauvegarde malgré tout');
console.log(
'⏭️ Sauvegarde sautée: Aucun changement détecté depuis la dernière sauvegarde'
);
console.log(
' 💡 Utilisez --force pour créer une sauvegarde malgré tout'
);
return;
}
@@ -200,13 +210,17 @@ OPTIONS:
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));
for (const backup of backups) {
const name = backup.filename.padEnd(40);
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);
console.log(`${name} ${size} ${type} ${date}`);
@@ -230,7 +244,10 @@ OPTIONS:
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(
`Restaurer la base de données depuis "${filename}" ? ATTENTION: Cela remplacera toutes les données actuelles !`,
force
@@ -247,24 +264,32 @@ OPTIONS:
}
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();
console.log('✅ Base de données vérifiée avec succès');
}
private async showConfig(): Promise<void> {
const config = backupService.getConfig();
const config = await backupService.getConfig();
const status = backupScheduler.getStatus();
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(` 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(`\n📊 Statut du planificateur:`);
console.log(` En cours: ${status.isRunning ? '✅ Oui' : '❌ Non'}`);
console.log(` Prochaine: ${status.nextBackup ? this.formatDate(status.nextBackup) : 'Non planifiée'}`);
console.log(
` 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> {
@@ -283,7 +308,9 @@ OPTIONS:
break;
case 'interval':
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);
}
newConfig.interval = value as BackupConfig['interval'];
@@ -328,10 +355,16 @@ OPTIONS:
const status = backupScheduler.getStatus();
console.log('📊 Statut du planificateur:\n');
console.log(` État: ${status.isRunning ? '✅ Actif' : '❌ Arrêté'}`);
console.log(` Activé: ${status.isEnabled ? '✅ Oui' : '❌ Non'}`);
console.log(
` État: ${status.isRunning ? '✅ Actif' : '❌ Arrêté'}`
);
console.log(
` Activé: ${status.isEnabled ? '✅ Oui' : '❌ Non'}`
);
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}`);
}
}

View File

@@ -21,7 +21,7 @@ function displayCacheStats() {
}
console.log('\n📋 Projets en cache:');
stats.projects.forEach(project => {
stats.projects.forEach((project) => {
const status = project.isExpired ? '❌ EXPIRÉ' : '✅ VALIDE';
console.log(`${project.projectKey}:`);
console.log(` - Âge: ${project.age}`);
@@ -93,11 +93,13 @@ async function main() {
// Interface interactive simple
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
output: process.stdout,
});
const askAction = () => {
rl.question('\nChoisissez une action (1-5): ', async (answer: string) => {
rl.question(
'\nChoisissez une action (1-5): ',
async (answer: string) => {
switch (answer.trim()) {
case '1':
displayCacheStats();
@@ -126,7 +128,8 @@ async function main() {
console.log('❌ Action invalide');
askAction();
}
});
}
);
};
askAction();

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

@@ -10,8 +10,12 @@ async function resetDatabase() {
try {
// Compter les tâches avant suppression
const beforeCount = await prisma.task.count();
const manualCount = await prisma.task.count({ where: { source: 'manual' } });
const remindersCount = await prisma.task.count({ where: { source: 'reminders' } });
const manualCount = await prisma.task.count({
where: { source: 'manual' },
});
const remindersCount = await prisma.task.count({
where: { source: 'reminders' },
});
console.log(`📊 État actuel:`);
console.log(` Total: ${beforeCount} tâches`);
@@ -22,8 +26,8 @@ async function resetDatabase() {
// Supprimer toutes les tâches de synchronisation
const deletedTasks = await prisma.task.deleteMany({
where: {
source: 'reminders'
}
source: 'reminders',
},
});
console.log(`✅ Supprimé ${deletedTasks.count} tâches de synchronisation`);
@@ -51,30 +55,32 @@ async function resetDatabase() {
include: {
taskTags: {
include: {
tag: true
}
}
tag: true,
},
orderBy: { createdAt: 'desc' }
},
},
orderBy: { createdAt: 'desc' },
});
remainingTasks.forEach((task, index) => {
const statusEmoji = {
'todo': '⏳',
'in_progress': '🔄',
'done': '',
'cancelled': '❌'
const statusEmoji =
{
todo: '',
in_progress: '🔄',
done: '✅',
cancelled: '❌',
}[task.status] || '❓';
// 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(', ')}]` : '';
console.log(` ${index + 1}. ${statusEmoji} ${task.title}${tagsStr}`);
});
}
} catch (error) {
console.error('❌ Erreur lors du reset:', error);
throw error;
@@ -83,11 +89,13 @@ async function resetDatabase() {
// Exécuter le script
if (require.main === module) {
resetDatabase().then(() => {
resetDatabase()
.then(() => {
console.log('');
console.log('✨ Reset terminé avec succès !');
process.exit(0);
}).catch((error) => {
})
.catch((error) => {
console.error('💥 Erreur fatale:', error);
process.exit(1);
});

View File

@@ -1,5 +1,6 @@
import { tasksService } from '../src/services/task-management/tasks';
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é
@@ -8,22 +9,46 @@ async function seedTestData() {
console.log('🌱 Ajout de données de test...');
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 = [
{
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,
priority: 'high' as TaskPriority,
tags: ['design', 'ui', 'frontend'],
dueDate: new Date('2025-12-31')
dueDate: new Date('2025-12-31'),
},
{
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,
priority: 'medium' as TaskPriority,
tags: ['backend', 'performance', 'api'],
dueDate: new Date('2025-12-15')
dueDate: new Date('2025-12-15'),
},
{
title: '✅ Test Coverage Improvement',
@@ -31,7 +56,7 @@ async function seedTestData() {
status: 'todo' as TaskStatus,
priority: 'medium' as TaskPriority,
tags: ['testing', 'quality'],
dueDate: new Date('2025-12-20')
dueDate: new Date('2025-12-20'),
},
{
title: '📱 Mobile Responsive Design',
@@ -39,7 +64,7 @@ async function seedTestData() {
status: 'todo' as TaskStatus,
priority: 'high' as TaskPriority,
tags: ['frontend', 'mobile', 'ui'],
dueDate: new Date('2025-12-10')
dueDate: new Date('2025-12-10'),
},
{
title: '🔒 Security Audit',
@@ -47,8 +72,8 @@ async function seedTestData() {
status: 'backlog' as TaskStatus,
priority: 'urgent' as TaskPriority,
tags: ['security', 'audit'],
dueDate: new Date('2026-01-15')
}
dueDate: new Date('2026-01-15'),
},
];
let createdCount = 0;
@@ -56,35 +81,43 @@ async function seedTestData() {
for (const taskData of testTasks) {
try {
const task = await tasksService.createTask(taskData);
const task = await tasksService.createTask({
...taskData,
ownerId: userId, // Ajouter l'ownerId
});
const statusEmoji = {
'backlog': '📋',
'todo': '⏳',
'in_progress': '🔄',
'freeze': '🧊',
'done': '✅',
'cancelled': '❌',
'archived': '📦'
backlog: '📋',
todo: '⏳',
in_progress: '🔄',
freeze: '🧊',
done: '✅',
cancelled: '❌',
archived: '📦',
}[task.status];
const priorityEmoji = {
'low': '🔵',
'medium': '🟡',
'high': '🔴',
'urgent': '🚨'
low: '🔵',
medium: '🟡',
high: '🔴',
urgent: '🚨',
}[task.priority];
console.log(` ${statusEmoji} ${priorityEmoji} ${task.title}`);
console.log(` Tags: ${task.tags?.join(', ') || 'aucun'}`);
if (task.dueDate) {
console.log(` Échéance: ${task.dueDate.toLocaleDateString('fr-FR')}`);
console.log(
` Échéance: ${task.dueDate.toLocaleDateString('fr-FR')}`
);
}
console.log('');
createdCount++;
} 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++;
}
}
@@ -94,7 +127,7 @@ async function seedTestData() {
console.log(` ❌ Erreurs: ${errorCount}`);
// Afficher les stats finales
const stats = await tasksService.getTaskStats();
const stats = await tasksService.getTaskStats(userId);
console.log('');
console.log('📈 Statistiques finales:');
console.log(` Total: ${stats.total} tâches`);
@@ -107,11 +140,13 @@ async function seedTestData() {
// Exécuter le script
if (require.main === module) {
seedTestData().then(() => {
seedTestData()
.then(() => {
console.log('');
console.log('✨ Données de test ajoutées avec succès !');
process.exit(0);
}).catch((error) => {
})
.catch((error) => {
console.error('💥 Erreur fatale:', error);
process.exit(1);
});

View File

@@ -1,7 +1,22 @@
import { PrismaClient } from '@prisma/client';
import { tagsService } from '../src/services/task-management/tags';
const prisma = new PrismaClient();
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 = [
{ name: 'frontend', color: '#3B82F6' },
@@ -19,9 +34,15 @@ async function seedTags() {
for (const tagData of testTags) {
try {
const existing = await tagsService.getTagByName(tagData.name);
const existing = await tagsService.getTagByName(
tagData.name,
firstUser.id
);
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})`);
} else {
console.log(`⚠️ Tag existe déjà: ${tagData.name}`);

View File

@@ -12,10 +12,16 @@ async function testJiraFields() {
console.log('🔍 Identification des champs personnalisés Jira\n');
try {
// Récupérer la config Jira
const jiraConfig = await userPreferencesService.getJiraConfig();
// 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) {
if (
!jiraConfig.enabled ||
!jiraConfig.baseUrl ||
!jiraConfig.email ||
!jiraConfig.apiToken
) {
console.log('❌ Configuration Jira manquante');
return;
}
@@ -45,14 +51,20 @@ async function testJiraFields() {
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🎯 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(
'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)');
@@ -72,7 +84,6 @@ async function testJiraFields() {
console.log('• Task: 3 points');
console.log('• Bug: 2 points');
console.log('• Subtask: 1 point');
} catch (error) {
console.error('❌ Erreur lors du test:', error);
}
@@ -80,4 +91,3 @@ async function testJiraFields() {
// 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

@@ -12,10 +12,16 @@ async function testStoryPoints() {
console.log('🧪 Test de récupération des story points Jira\n');
try {
// Récupérer la config Jira
const jiraConfig = await userPreferencesService.getJiraConfig();
// 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) {
if (
!jiraConfig.enabled ||
!jiraConfig.baseUrl ||
!jiraConfig.email ||
!jiraConfig.apiToken
) {
console.log('❌ Configuration Jira manquante');
return;
}
@@ -41,7 +47,10 @@ async function testStoryPoints() {
let ticketsWithoutStoryPoints = 0;
const storyPointsDistribution: Record<number, number> = {};
const typeDistribution: Record<string, { count: number; totalPoints: number }> = {};
const typeDistribution: Record<
string,
{ count: number; totalPoints: number }
> = {};
issues.slice(0, 20).forEach((issue, index) => {
const storyPoints = issue.storyPoints || 0;
@@ -49,14 +58,17 @@ async function testStoryPoints() {
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(
` 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;
storyPointsDistribution[storyPoints] =
(storyPointsDistribution[storyPoints] || 0) + 1;
} else {
ticketsWithoutStoryPoints++;
}
@@ -74,7 +86,9 @@ async function testStoryPoints() {
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(
`Moyenne par ticket: ${issues.length > 0 ? (totalStoryPoints / issues.length).toFixed(2) : 0}`
);
console.log('\n📊 Distribution des story points:');
Object.entries(storyPointsDistribution)
@@ -85,20 +99,28 @@ async function testStoryPoints() {
console.log('\n🏷 Distribution par type:');
Object.entries(typeDistribution)
.sort(([,a], [,b]) => b.count - a.count)
.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`);
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(
'• 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');
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);
}
@@ -106,4 +128,3 @@ async function testStoryPoints() {
// Exécution du script
testStoryPoints().catch(console.error);

View File

@@ -14,20 +14,24 @@ export async function createBackupAction(force: boolean = false) {
return {
success: true,
skipped: true,
message: 'Sauvegarde sautée : aucun changement détecté. Utilisez "Forcer" pour créer malgré tout.'
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}`
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'
error:
error instanceof Error
? error.message
: 'Erreur lors de la création de la sauvegarde',
};
}
}
@@ -37,13 +41,13 @@ export async function verifyDatabaseAction() {
await backupService.verifyDatabaseHealth();
return {
success: true,
message: 'Intégrité vérifiée'
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'
error: error instanceof Error ? error.message : 'Vérification échouée',
};
}
}

View File

@@ -1,9 +1,20 @@
'use server';
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 { 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
@@ -14,28 +25,13 @@ export async function toggleCheckbox(checkboxId: string): Promise<{
error?: string;
}> {
try {
// Nous devons d'abord récupérer la checkbox pour connaître son état actuel
// En absence de getCheckboxById, nous allons essayer de la trouver via une vue daily
// Pour l'instant, nous allons simplement toggle via updateCheckbox
// (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);
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
if (!checkbox) {
return { success: false, error: 'Checkbox non trouvée' };
}
// Toggle l'état
const updatedCheckbox = await dailyService.updateCheckbox(checkboxId, {
isChecked: !checkbox.isChecked
});
// Toggle direct côté service par ID (indépendant de la date)
const updatedCheckbox = await dailyService.toggleCheckbox(checkboxId);
revalidatePath('/daily');
return { success: true, data: updatedCheckbox };
@@ -43,26 +39,38 @@ export async function toggleCheckbox(checkboxId: string): Promise<{
console.error('Erreur toggleCheckbox:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
error: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}
/**
* 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;
data?: DailyCheckbox;
error?: string;
}> {
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({
date: getToday(),
date: targetDate,
userId: session.user.id,
text: content,
type: type || 'task',
taskId
taskId,
});
revalidatePath('/daily');
@@ -71,7 +79,7 @@ export async function addTodayCheckbox(content: string, type?: 'task' | 'meeting
console.error('Erreur addTodayCheckbox:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
error: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}
@@ -79,19 +87,31 @@ export async function addTodayCheckbox(content: string, type?: 'task' | 'meeting
/**
* 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;
data?: DailyCheckbox;
error?: string;
}> {
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({
date: yesterday,
userId: session.user.id,
text: content,
type: type || 'task',
taskId
taskId,
});
revalidatePath('/daily');
@@ -100,16 +120,18 @@ export async function addYesterdayCheckbox(content: string, type?: 'task' | 'mee
console.error('Erreur addYesterdayCheckbox:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
error: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}
/**
* 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;
data?: DailyCheckbox;
error?: string;
@@ -123,7 +145,7 @@ export async function updateCheckbox(checkboxId: string, data: UpdateDailyCheckb
console.error('Erreur updateCheckbox:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
error: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}
@@ -144,7 +166,7 @@ export async function deleteCheckbox(checkboxId: string): Promise<{
console.error('Erreur deleteCheckbox:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
error: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}
@@ -152,20 +174,30 @@ export async function deleteCheckbox(checkboxId: string): Promise<{
/**
* 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;
data?: DailyCheckbox;
error?: string;
}> {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
const targetDate = normalizeDate(date || getToday());
const checkboxData: CreateDailyCheckboxData = {
date: targetDate,
userId: session.user.id,
text: text.trim(),
type: 'task',
taskId: taskId,
isChecked: false
isChecked: false,
};
const checkbox = await dailyService.addCheckbox(checkboxData);
@@ -177,7 +209,7 @@ export async function addTodoToTask(taskId: string, text: string, date?: Date):
console.error('Erreur addTodoToTask:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
error: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}
@@ -185,7 +217,10 @@ export async function addTodoToTask(taskId: string, text: string, date?: 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;
error?: string;
}> {
@@ -201,7 +236,7 @@ export async function reorderCheckboxes(dailyId: string, checkboxIds: string[]):
console.error('Erreur reorderCheckboxes:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
error: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}
@@ -223,7 +258,7 @@ export async function moveCheckboxToToday(checkboxId: string): Promise<{
console.error('Erreur moveCheckboxToToday:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
error: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}

132
src/actions/folders.ts Normal file
View File

@@ -0,0 +1,132 @@
'use server';
import {
foldersService,
CreateFolderData,
UpdateFolderData,
} from '@/services/folders';
import { revalidatePath } from 'next/cache';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
/**
* Récupère tous les dossiers de l'utilisateur
*/
export async function getFolders() {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non autorisé' };
}
const folders = await foldersService.getFolders(session.user.id);
return { success: true, data: folders };
} catch (error) {
console.error('Error fetching folders:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}
/**
* Crée un nouveau dossier
*/
export async function createFolder(data: Omit<CreateFolderData, 'userId'>) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non autorisé' };
}
const folder = await foldersService.createFolder({
...data,
userId: session.user.id,
});
revalidatePath('/notes');
return { success: true, data: folder };
} catch (error) {
console.error('Error creating folder:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}
/**
* Met à jour un dossier
*/
export async function updateFolder(folderId: string, data: UpdateFolderData) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non autorisé' };
}
const folder = await foldersService.updateFolder(
folderId,
session.user.id,
data
);
revalidatePath('/notes');
return { success: true, data: folder };
} catch (error) {
console.error('Error updating folder:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}
/**
* Supprime un dossier
*/
export async function deleteFolder(folderId: string) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non autorisé' };
}
await foldersService.deleteFolder(folderId, session.user.id);
revalidatePath('/notes');
return { success: true };
} catch (error) {
console.error('Error deleting folder:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}
/**
* Réorganise l'ordre des dossiers
*/
export async function reorderFolders(
folderOrders: Array<{ id: string; order: number }>
) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non autorisé' };
}
await foldersService.reorderFolders(session.user.id, folderOrders);
revalidatePath('/notes');
return { success: true };
} catch (error) {
console.error('Error reordering folders:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}

View File

@@ -3,6 +3,8 @@
import { JiraAnalyticsService } from '@/services/integrations/jira/analytics';
import { userPreferencesService } from '@/services/core/user-preferences';
import { JiraAnalytics } from '@/lib/types';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
export type JiraAnalyticsResult = {
success: boolean;
@@ -13,22 +15,38 @@ export type JiraAnalyticsResult = {
/**
* 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 {
// Récupérer la config Jira depuis la base de données
const jiraConfig = await userPreferencesService.getJiraConfig();
const session = await getServerSession(authOptions);
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 {
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) {
return {
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.',
};
}
@@ -38,7 +56,7 @@ export async function getJiraAnalytics(forceRefresh = false): Promise<JiraAnalyt
baseUrl: jiraConfig.baseUrl,
email: jiraConfig.email,
apiToken: jiraConfig.apiToken,
projectKey: jiraConfig.projectKey
projectKey: jiraConfig.projectKey,
});
// Récupérer les analytics (avec cache ou actualisation forcée)
@@ -46,15 +64,17 @@ export async function getJiraAnalytics(forceRefresh = false): Promise<JiraAnalyt
return {
success: true,
data: analytics
data: analytics,
};
} catch (error) {
console.error('❌ Erreur lors du calcul des analytics Jira:', error);
return {
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';
import { jiraAnomalyDetection, JiraAnomaly, AnomalyDetectionConfig } from '@/services/integrations/jira/anomaly-detection';
import { JiraAnalyticsService, JiraAnalyticsConfig } from '@/services/integrations/jira/analytics';
import {
jiraAnomalyDetection,
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 {
success: boolean;
@@ -13,15 +22,29 @@ export interface AnomalyDetectionResult {
/**
* 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 {
// Récupérer la config Jira
const jiraConfig = await userPreferencesService.getJiraConfig();
const session = await getServerSession(authOptions);
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 {
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' };
}
const analyticsService = new JiraAnalyticsService(jiraConfig as JiraAnalyticsConfig);
const analyticsService = new JiraAnalyticsService(
jiraConfig as JiraAnalyticsConfig
);
const analytics = await analyticsService.getProjectAnalytics(forceRefresh);
// Détecter les anomalies
@@ -38,13 +63,13 @@ export async function detectJiraAnomalies(forceRefresh = false): Promise<Anomaly
return {
success: true,
data: anomalies
data: anomalies,
};
} 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 {
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
*/
export async function updateAnomalyDetectionConfig(config: Partial<AnomalyDetectionConfig>) {
export async function updateAnomalyDetectionConfig(
config: Partial<AnomalyDetectionConfig>
) {
try {
jiraAnomalyDetection.updateConfig(config);
return {
success: true,
data: jiraAnomalyDetection.getConfig()
data: jiraAnomalyDetection.getConfig(),
};
} catch (error) {
console.error('❌ Erreur lors de la mise à jour de la config:', error);
return {
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 {
return {
success: true,
data: jiraAnomalyDetection.getConfig()
data: jiraAnomalyDetection.getConfig(),
};
} catch (error) {
console.error('❌ Erreur lors de la récupération de la config:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
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
*/
export async function exportJiraAnalytics(format: ExportFormat = 'csv'): Promise<ExportResult> {
export async function exportJiraAnalytics(
format: ExportFormat = 'csv'
): Promise<ExportResult> {
try {
// Récupérer les analytics (force refresh pour avoir les données les plus récentes)
const analyticsResult = await getJiraAnalytics(true);
@@ -99,7 +101,7 @@ export async function exportJiraAnalytics(format: ExportFormat = 'csv'): Promise
if (!analyticsResult.success || !analyticsResult.data) {
return {
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 {
success: true,
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 {
success: true,
data: csvData,
filename: `jira-analytics-${projectKey}-${timestamp}.csv`
filename: `jira-analytics-${projectKey}-${timestamp}.csv`,
};
} catch (error) {
console.error('❌ Erreur lors de l\'export des analytics:', error);
console.error("❌ Erreur lors de l'export des analytics:", error);
return {
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
lines.push('# Rapport Analytics Jira');
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('');
// Section 1: Métriques d'équipe
lines.push('## Répartition de l\'équipe');
lines.push('Assignee,Nom,Total Tickets,Tickets Complétés,Tickets En Cours,Pourcentage');
analytics.teamMetrics.issuesDistribution.forEach((assignee: AssigneeMetrics) => {
lines.push([
lines.push("## Répartition de l'équipe");
lines.push(
'Assignee,Nom,Total Tickets,Tickets Complétés,Tickets En Cours,Pourcentage'
);
analytics.teamMetrics.issuesDistribution.forEach(
(assignee: AssigneeMetrics) => {
lines.push(
[
escapeCsv(assignee.assignee),
escapeCsv(assignee.displayName),
assignee.totalIssues,
assignee.completedIssues,
assignee.inProgressIssues,
assignee.percentage.toFixed(1) + '%'
].join(','));
});
assignee.percentage.toFixed(1) + '%',
].join(',')
);
}
);
lines.push('');
// Section 2: 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) => {
lines.push([
lines.push(
[
escapeCsv(sprint.sprintName),
sprint.startDate.slice(0, 10),
sprint.endDate.slice(0, 10),
sprint.plannedPoints,
sprint.completedPoints,
sprint.completionRate + '%'
].join(','));
sprint.completionRate + '%',
].join(',')
);
});
lines.push('');
// Section 3: Cycle time par type
lines.push('## Cycle Time par type de ticket');
lines.push('Type de Ticket,Temps Moyen (jours),Temps Médian (jours),Échantillons');
analytics.cycleTimeMetrics.cycleTimeByType.forEach((type: CycleTimeByType) => {
lines.push([
lines.push(
'Type de Ticket,Temps Moyen (jours),Temps Médian (jours),Échantillons'
);
analytics.cycleTimeMetrics.cycleTimeByType.forEach(
(type: CycleTimeByType) => {
lines.push(
[
escapeCsv(type.issueType),
type.averageDays,
type.medianDays,
type.samples
].join(','));
});
type.samples,
].join(',')
);
}
);
lines.push('');
// Section 4: Work in Progress
lines.push('## Work in Progress par statut');
lines.push('Statut,Nombre,Pourcentage');
analytics.workInProgress.byStatus.forEach((status: WorkInProgressStatus) => {
lines.push([
escapeCsv(status.status),
status.count,
status.percentage + '%'
].join(','));
lines.push(
[escapeCsv(status.status), status.count, status.percentage + '%'].join(
','
)
);
});
lines.push('');
// Section 5: Charge de travail par assignee
lines.push('## Charge de travail par assignee');
lines.push('Assignee,Nom,À Faire,En Cours,En Revue,Total Actif');
analytics.workInProgress.byAssignee.forEach((assignee: WorkInProgressAssignee) => {
lines.push([
analytics.workInProgress.byAssignee.forEach(
(assignee: WorkInProgressAssignee) => {
lines.push(
[
escapeCsv(assignee.assignee),
escapeCsv(assignee.displayName),
assignee.todoCount,
assignee.inProgressCount,
assignee.reviewCount,
assignee.totalActive
].join(','));
});
assignee.totalActive,
].join(',')
);
}
);
lines.push('');
// Section 6: Métriques résumé
lines.push('## Métriques de résumé');
lines.push('Métrique,Valeur');
lines.push([
'Total membres équipe',
analytics.teamMetrics.totalAssignees
].join(','));
lines.push([
'Membres actifs',
analytics.teamMetrics.activeAssignees
].join(','));
lines.push([
lines.push(
['Total membres équipe', analytics.teamMetrics.totalAssignees].join(',')
);
lines.push(
['Membres actifs', analytics.teamMetrics.activeAssignees].join(',')
);
lines.push(
[
'Points complétés sprint actuel',
analytics.velocityMetrics.currentSprintPoints
].join(','));
lines.push([
'Vélocité moyenne',
analytics.velocityMetrics.averageVelocity
].join(','));
lines.push([
analytics.velocityMetrics.currentSprintPoints,
].join(',')
);
lines.push(
['Vélocité moyenne', analytics.velocityMetrics.averageVelocity].join(',')
);
lines.push(
[
'Cycle time moyen (jours)',
analytics.cycleTimeMetrics.averageCycleTime
].join(','));
analytics.cycleTimeMetrics.averageCycleTime,
].join(',')
);
return lines.join('\n');
}

View File

@@ -1,9 +1,18 @@
'use server';
import { JiraAnalyticsService, JiraAnalyticsConfig } from '@/services/integrations/jira/analytics';
import {
JiraAnalyticsService,
JiraAnalyticsConfig,
} 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 {
AvailableFilters,
JiraAnalyticsFilters,
JiraAnalytics,
} from '@/lib/types';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
export interface FiltersResult {
success: boolean;
@@ -22,13 +31,25 @@ export interface FilteredAnalyticsResult {
*/
export async function getAvailableJiraFilters(): Promise<FiltersResult> {
try {
// Récupérer la config Jira
const jiraConfig = await userPreferencesService.getJiraConfig();
const session = await getServerSession(authOptions);
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 {
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' };
}
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
const allIssues = await analyticsService.getAllProjectIssues();
// Extraire les filtres disponibles
const availableFilters = JiraAdvancedFiltersService.extractAvailableFilters(allIssues);
const availableFilters =
JiraAdvancedFiltersService.extractAvailableFilters(allIssues);
return {
success: true,
data: availableFilters
data: availableFilters,
};
} catch (error) {
console.error('❌ Erreur lors de la récupération des filtres:', error);
return {
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
*/
export async function getFilteredJiraAnalytics(filters: Partial<JiraAnalyticsFilters>): Promise<FilteredAnalyticsResult> {
export async function getFilteredJiraAnalytics(
filters: Partial<JiraAnalyticsFilters>
): Promise<FilteredAnalyticsResult> {
try {
// Récupérer la config Jira
const jiraConfig = await userPreferencesService.getJiraConfig();
const session = await getServerSession(authOptions);
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 {
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' };
}
const analyticsService = new JiraAnalyticsService(jiraConfig as JiraAnalyticsConfig);
const analyticsService = new JiraAnalyticsService(
jiraConfig as JiraAnalyticsConfig
);
const originalAnalytics = await analyticsService.getProjectAnalytics();
// Si aucun filtre actif, retourner les données originales
if (!JiraAdvancedFiltersService.hasActiveFilters(filters)) {
return {
success: true,
data: originalAnalytics
data: originalAnalytics,
};
}
@@ -93,7 +133,8 @@ export async function getFilteredJiraAnalytics(filters: Partial<JiraAnalyticsFil
const allIssues = await analyticsService.getAllProjectIssues();
// Appliquer les filtres
const filteredAnalytics = JiraAdvancedFiltersService.applyFiltersToAnalytics(
const filteredAnalytics =
JiraAdvancedFiltersService.applyFiltersToAnalytics(
originalAnalytics,
filters,
allIssues
@@ -101,13 +142,13 @@ export async function getFilteredJiraAnalytics(filters: Partial<JiraAnalyticsFil
return {
success: true,
data: filteredAnalytics
data: filteredAnalytics,
};
} catch (error) {
console.error('❌ Erreur lors du filtrage des analytics:', error);
return {
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';
import { JiraAnalyticsService, JiraAnalyticsConfig } from '@/services/integrations/jira/analytics';
import {
JiraAnalyticsService,
JiraAnalyticsConfig,
} from '@/services/integrations/jira/analytics';
import { userPreferencesService } from '@/services/core/user-preferences';
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 { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
export interface SprintDetailsResult {
success: boolean;
@@ -15,15 +25,29 @@ export interface SprintDetailsResult {
/**
* 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 {
// Récupérer la config Jira
const jiraConfig = await userPreferencesService.getJiraConfig();
const session = await getServerSession(authOptions);
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 {
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' };
}
const analyticsService = new JiraAnalyticsService(jiraConfig as JiraAnalyticsConfig);
const analyticsService = new JiraAnalyticsService(
jiraConfig as JiraAnalyticsConfig
);
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) {
return {
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 sprintEnd = parseDate(sprint.endDate);
const sprintIssues = allIssues.filter(issue => {
const sprintIssues = allIssues.filter((issue) => {
const issueDate = parseDate(issue.created);
return issueDate >= sprintStart && issueDate <= sprintEnd;
});
@@ -71,18 +99,21 @@ export async function getSprintDetails(sprintName: string): Promise<SprintDetail
issues: sprintIssues,
assigneeDistribution,
statusDistribution,
metrics: sprintMetrics
metrics: sprintMetrics,
};
return {
success: true,
data: sprintDetails
data: sprintDetails,
};
} 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 {
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) {
const totalIssues = issues.length;
const completedIssues = issues.filter(issue =>
const completedIssues = issues.filter(
(issue) =>
issue.status.category === 'Done' ||
issue.status.name.toLowerCase().includes('done') ||
issue.status.name.toLowerCase().includes('closed')
).length;
const inProgressIssues = issues.filter(issue =>
const inProgressIssues = issues.filter(
(issue) =>
issue.status.category === 'In Progress' ||
issue.status.name.toLowerCase().includes('progress') ||
issue.status.name.toLowerCase().includes('review')
).length;
const blockedIssues = issues.filter(issue =>
const blockedIssues = issues.filter(
(issue) =>
issue.status.name.toLowerCase().includes('blocked') ||
issue.status.name.toLowerCase().includes('waiting')
).length;
// 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
);
@@ -119,7 +154,8 @@ function calculateSprintMetrics(issues: JiraTask[], sprint: SprintVelocity) {
const totalCycleTime = completedIssuesWithDates.reduce((total, issue) => {
const created = parseDate(issue.created);
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;
}, 0);
averageCycleTime = totalCycleTime / completedIssuesWithDates.length;
@@ -139,19 +175,28 @@ function calculateSprintMetrics(issues: JiraTask[], sprint: SprintVelocity) {
inProgressIssues,
blockedIssues,
averageCycleTime,
velocityTrend
velocityTrend,
};
}
/**
* Calcule la distribution par assigné pour le sprint
*/
function calculateAssigneeDistribution(issues: JiraTask[]): AssigneeDistribution[] {
const assigneeMap = new Map<string, { total: number; completed: number; inProgress: number }>();
function calculateAssigneeDistribution(
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 current = assigneeMap.get(assigneeName) || { total: 0, completed: 0, inProgress: 0 };
const current = assigneeMap.get(assigneeName) || {
total: 0,
completed: 0,
inProgress: 0,
};
current.total++;
@@ -164,15 +209,17 @@ function calculateAssigneeDistribution(issues: JiraTask[]): AssigneeDistribution
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,
displayName,
totalIssues: stats.total,
completedIssues: stats.completed,
inProgressIssues: stats.inProgress,
percentage: issues.length > 0 ? (stats.total / issues.length) * 100 : 0,
count: stats.total // Ajout pour compatibilité
})).sort((a, b) => b.totalIssues - a.totalIssues);
count: stats.total, // Ajout pour compatibilité
}))
.sort((a, b) => b.totalIssues - a.totalIssues);
}
/**
@@ -181,13 +228,18 @@ function calculateAssigneeDistribution(issues: JiraTask[]): AssigneeDistribution
function calculateStatusDistribution(issues: JiraTask[]): StatusDistribution[] {
const statusMap = new Map<string, number>();
issues.forEach(issue => {
statusMap.set(issue.status.name, (statusMap.get(issue.status.name) || 0) + 1);
issues.forEach((issue) => {
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,
count,
percentage: issues.length > 0 ? (count / issues.length) * 100 : 0
})).sort((a, b) => b.count - a.count);
percentage: issues.length > 0 ? (count / issues.length) * 100 : 0,
}))
.sort((a, b) => b.count - a.count);
}

View File

@@ -1,7 +1,13 @@
'use server';
import { MetricsService, WeeklyMetricsOverview, VelocityTrend } from '@/services/analytics/metrics';
import {
MetricsService,
WeeklyMetricsOverview,
VelocityTrend,
} from '@/services/analytics/metrics';
import { getToday } from '@/lib/date-utils';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
/**
* Récupère les métriques hebdomadaires pour une date donnée
@@ -12,18 +18,33 @@ export async function getWeeklyMetrics(date?: Date): Promise<{
error?: string;
}> {
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 metrics = await MetricsService.getWeeklyMetrics(targetDate);
const metrics = await MetricsService.getWeeklyMetrics(
session.user.id,
targetDate
);
return {
success: true,
data: metrics
data: metrics,
};
} catch (error) {
console.error('Error fetching weekly metrics:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch weekly metrics'
error:
error instanceof Error
? error.message
: 'Failed to fetch weekly metrics',
};
}
}
@@ -37,25 +58,39 @@ export async function getVelocityTrends(weeksBack: number = 4): Promise<{
error?: string;
}> {
try {
if (weeksBack < 1 || weeksBack > 12) {
// Récupérer l'utilisateur connecté
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return {
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 {
success: true,
data: trends
data: trends,
};
} catch (error) {
console.error('Error fetching velocity trends:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch velocity trends'
error:
error instanceof Error
? error.message
: 'Failed to fetch velocity trends',
};
}
}

63
src/actions/notes.ts Normal file
View File

@@ -0,0 +1,63 @@
'use server';
import { notesService } from '@/services/notes';
import { revalidatePath } from 'next/cache';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
/**
* Réordonne les notes
*/
export async function reorderNotes(
noteOrders: Array<{ id: string; order: number }>
) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non autorisé' };
}
await notesService.reorderNotes(session.user.id, noteOrders);
revalidatePath('/notes');
return { success: true };
} catch (error) {
console.error('Error reordering notes:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}
/**
* Bascule l'état favori d'une note
*/
export async function toggleNoteFavorite(noteId: string) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non autorisé' };
}
// Récupérer la note actuelle pour connaître son état favori
const currentNote = await notesService.getNoteById(noteId, session.user.id);
if (!currentNote) {
return { success: false, error: 'Note non trouvée' };
}
// Basculer l'état favori
const updatedNote = await notesService.updateNote(noteId, session.user.id, {
isFavorite: !currentNote.isFavorite,
});
revalidatePath('/notes');
return { success: true, note: updatedNote };
} catch (error) {
console.error('Error toggling note favorite:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}

View File

@@ -1,26 +1,72 @@
'use server';
import { userPreferencesService } from '@/services/core/user-preferences';
import { KanbanFilters, ViewPreferences, ColumnVisibility, TaskStatus } from '@/lib/types';
import { Theme } from '@/lib/theme-config';
import {
KanbanFilters,
ViewPreferences,
ColumnVisibility,
TaskStatus,
} from '@/lib/types';
import { Theme } from '@/lib/ui-config';
import { revalidatePath } from 'next/cache';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
/**
* 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;
error?: string;
}> {
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('/');
return { success: true };
} catch (error) {
console.error('Erreur updateViewPreferences:', error);
return {
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',
};
}
}
@@ -28,19 +74,26 @@ export async function updateViewPreferences(updates: Partial<ViewPreferences>):
/**
* Met à jour les filtres Kanban
*/
export async function updateKanbanFilters(updates: Partial<KanbanFilters>): Promise<{
export async function updateKanbanFilters(
updates: Partial<KanbanFilters>
): Promise<{
success: boolean;
error?: string;
}> {
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');
return { success: true };
} catch (error) {
console.error('Erreur updateKanbanFilters:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
error: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}
@@ -48,25 +101,37 @@ export async function updateKanbanFilters(updates: Partial<KanbanFilters>): Prom
/**
* Met à jour la visibilité des colonnes
*/
export async function updateColumnVisibility(updates: Partial<ColumnVisibility>): Promise<{
export async function updateColumnVisibility(
updates: Partial<ColumnVisibility>
): Promise<{
success: boolean;
error?: string;
}> {
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 = {
...preferences.columnVisibility,
...updates
...updates,
};
await userPreferencesService.saveColumnVisibility(newColumnVisibility);
await userPreferencesService.saveColumnVisibility(
session.user.id,
newColumnVisibility
);
revalidatePath('/kanban');
return { success: true };
} catch (error) {
console.error('Erreur updateColumnVisibility:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
error: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}
@@ -79,17 +144,26 @@ export async function toggleObjectivesVisibility(): Promise<{
error?: string;
}> {
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;
await userPreferencesService.updateViewPreferences({ showObjectives });
await userPreferencesService.updateViewPreferences(session.user.id, {
showObjectives,
});
revalidatePath('/');
return { success: true };
} catch (error) {
console.error('Erreur toggleObjectivesVisibility:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
error: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}
@@ -102,17 +176,26 @@ export async function toggleObjectivesCollapse(): Promise<{
error?: string;
}> {
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;
await userPreferencesService.updateViewPreferences({ collapseObjectives });
await userPreferencesService.updateViewPreferences(session.user.id, {
collapseObjectives,
});
revalidatePath('/');
return { success: true };
} catch (error) {
console.error('Erreur toggleObjectivesCollapse:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
error: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}
@@ -125,14 +208,21 @@ export async function setTheme(theme: Theme): Promise<{
error?: string;
}> {
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('/');
return { success: true };
} catch (error) {
console.error('Erreur setTheme:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
error: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}
@@ -145,17 +235,27 @@ export async function toggleTheme(): Promise<{
error?: string;
}> {
try {
const preferences = await userPreferencesService.getAllPreferences();
const newTheme = preferences.viewPreferences.theme === 'dark' ? 'light' : 'dark';
const session = await getServerSession(authOptions);
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('/');
return { success: true };
} catch (error) {
console.error('Erreur toggleTheme:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
error: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}
@@ -168,20 +268,35 @@ export async function toggleFontSize(): Promise<{
error?: string;
}> {
try {
const preferences = await userPreferencesService.getAllPreferences();
const fontSizes: ('small' | 'medium' | 'large')[] = ['small', 'medium', 'large'];
const currentIndex = fontSizes.indexOf(preferences.viewPreferences.fontSize);
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
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 newFontSize = fontSizes[nextIndex];
await userPreferencesService.updateViewPreferences({ fontSize: newFontSize });
await userPreferencesService.updateViewPreferences(session.user.id, {
fontSize: newFontSize,
});
revalidatePath('/');
return { success: true };
} catch (error) {
console.error('Erreur toggleFontSize:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
error: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}
@@ -194,7 +309,14 @@ export async function toggleColumnVisibility(status: TaskStatus): Promise<{
error?: string;
}> {
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);
if (hiddenStatuses.has(status)) {
@@ -203,8 +325,8 @@ export async function toggleColumnVisibility(status: TaskStatus): Promise<{
hiddenStatuses.add(status);
}
await userPreferencesService.saveColumnVisibility({
hiddenStatuses: Array.from(hiddenStatuses)
await userPreferencesService.saveColumnVisibility(session.user.id, {
hiddenStatuses: Array.from(hiddenStatuses),
});
revalidatePath('/kanban');
@@ -213,7 +335,7 @@ export async function toggleColumnVisibility(status: TaskStatus): Promise<{
console.error('Erreur toggleColumnVisibility:', error);
return {
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

@@ -10,7 +10,8 @@ export async function getSystemInfo() {
console.error('Error getting system info:', error);
return {
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

@@ -3,6 +3,8 @@
import { tagsService } from '@/services/task-management/tags';
import { revalidatePath } from 'next/cache';
import { Tag } from '@/lib/types';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/lib/auth';
export type ActionResult<T = void> = {
success: boolean;
@@ -18,7 +20,16 @@ export async function createTag(
color: string
): Promise<ActionResult<Tag>> {
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
revalidatePath('/');
@@ -30,7 +41,7 @@ export async function createTag(
console.error('Error creating tag:', error);
return {
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 }
): Promise<ActionResult<Tag>> {
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) {
return { success: false, error: 'Tag non trouvé' };
@@ -59,7 +75,7 @@ export async function updateTag(
console.error('Error updating tag:', error);
return {
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> {
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
revalidatePath('/');
@@ -81,8 +102,7 @@ export async function deleteTag(tagId: string): Promise<ActionResult> {
console.error('Error deleting tag:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to delete tag'
error: error instanceof Error ? error.message : 'Failed to delete tag',
};
}
}

View File

@@ -1,8 +1,10 @@
'use server'
'use server';
import { tasksService } from '@/services/task-management/tasks';
import { revalidatePath } from 'next/cache';
import { TaskStatus, TaskPriority } from '@/lib/types';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/lib/auth';
export type ActionResult<T = unknown> = {
success: boolean;
@@ -10,6 +12,30 @@ export type ActionResult<T = unknown> = {
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
*/
@@ -18,18 +44,30 @@ export async function updateTaskStatus(
status: TaskStatus
): Promise<ActionResult> {
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
revalidatePath('/');
revalidatePath('/tasks');
return { success: true, data: task };
return { success: true, data: updatedTask };
} catch (error) {
console.error('Error updating task status:', error);
return {
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' };
}
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
revalidatePath('/');
@@ -57,7 +106,8 @@ export async function updateTaskTitle(
console.error('Error updating task title:', error);
return {
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> {
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
revalidatePath('/');
@@ -78,7 +137,7 @@ export async function deleteTask(taskId: string): Promise<ActionResult> {
console.error('Error deleting task:', error);
return {
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;
priority?: TaskPriority;
tags?: string[];
primaryTagId?: string;
dueDate?: Date;
}): Promise<ActionResult> {
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> = {};
if (data.title !== undefined) {
@@ -105,13 +174,16 @@ export async function updateTask(data: {
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.priority !== undefined) updateData.priority = data.priority;
if (data.tags !== undefined) updateData.tags = data.tags;
if (data.primaryTagId !== undefined)
updateData.primaryTagId = data.primaryTagId;
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
revalidatePath('/');
@@ -122,7 +194,7 @@ export async function updateTask(data: {
console.error('Error updating task:', error);
return {
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;
priority?: TaskPriority;
tags?: string[];
primaryTagId?: string;
}): Promise<ActionResult> {
try {
if (!data.title.trim()) {
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({
title: data.title.trim(),
description: data.description?.trim() || '',
status: data.status || 'todo',
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
@@ -159,7 +237,7 @@ export async function createTask(data: {
console.error('Error creating task:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to create task'
error: error instanceof Error ? error.message : 'Failed to create task',
};
}
}

View File

@@ -2,14 +2,21 @@
import { userPreferencesService } from '@/services/core/user-preferences';
import { revalidatePath } from 'next/cache';
import { tfsService, TfsConfig } from '@/services/integrations/tfs';
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 {
await userPreferencesService.saveTfsConfig(config);
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();
@@ -34,7 +41,12 @@ export async function saveTfsConfig(config: TfsConfig) {
*/
export async function getTfsConfig() {
try {
const config = await userPreferencesService.getTfsConfig();
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);
@@ -64,7 +76,13 @@ export async function saveTfsSchedulerConfig(
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
);
@@ -90,8 +108,17 @@ export async function saveTfsSchedulerConfig(
*/
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();
const result = await tfsService.syncTasks(session.user.id);
if (result.success) {
revalidatePath('/');

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

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

View File

@@ -13,7 +13,7 @@ export async function GET(request: NextRequest) {
return NextResponse.json({
success: true,
data: { logs }
data: { logs },
});
}
@@ -23,14 +23,14 @@ export async function GET(request: NextRequest) {
return NextResponse.json({
success: true,
data: stats
data: stats,
});
}
console.log('🔄 API GET /api/backups called');
// Test de la configuration d'abord
const config = backupService.getConfig();
const config = await backupService.getConfig();
console.log('✅ Config loaded:', config);
// Test du scheduler
@@ -47,20 +47,24 @@ export async function GET(request: NextRequest) {
backups,
scheduler: schedulerStatus,
config,
}
},
};
console.log('✅ API response ready');
return NextResponse.json(response);
} catch (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(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch backups',
details: error instanceof Error ? error.stack : undefined
error:
error instanceof Error ? error.message : 'Failed to fetch backups',
details: error instanceof Error ? error.stack : undefined,
},
{ status: 500 }
);
@@ -81,7 +85,8 @@ export async function POST(request: NextRequest) {
return NextResponse.json({
success: 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.',
});
}
@@ -91,19 +96,22 @@ export async function POST(request: NextRequest) {
await backupService.verifyDatabaseHealth();
return NextResponse.json({
success: true,
message: 'Database health check passed'
message: 'Database health check passed',
});
case 'config':
await backupService.updateConfig(params.config);
// 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();
}
return NextResponse.json({
success: true,
message: 'Configuration updated',
data: backupService.getConfig()
data: await backupService.getConfig(),
});
case 'scheduler':
@@ -114,7 +122,7 @@ export async function POST(request: NextRequest) {
}
return NextResponse.json({
success: true,
data: backupScheduler.getStatus()
data: backupScheduler.getStatus(),
});
default:
@@ -128,7 +136,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
);

View File

@@ -1,5 +1,7 @@
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 avec des dailies
@@ -7,9 +9,13 @@ import { dailyService } from '@/services/task-management/daily';
*/
export async function GET() {
try {
const dates = await dailyService.getDailyDates();
return NextResponse.json({ dates });
const session = await getServerSession(authOptions);
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) {
console.error('Erreur lors de la récupération des dates:', error);
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

@@ -1,21 +1,34 @@
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 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 limit = searchParams.get('limit')
? parseInt(searchParams.get('limit')!)
: undefined;
const pendingCheckboxes = await dailyService.getPendingCheckboxes({
maxDays,
excludeToday,
type,
limit
limit,
userId: session.user.id, // Filtrer par user connecté
});
return NextResponse.json(pendingCheckboxes);

View File

@@ -1,12 +1,24 @@
import { NextResponse } from 'next/server';
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)
*/
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 action = searchParams.get('action');
@@ -15,7 +27,10 @@ export async function GET(request: Request) {
if (action === 'history') {
// Récupérer l'historique
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);
}
@@ -25,7 +40,10 @@ export async function GET(request: Request) {
const limit = parseInt(searchParams.get('limit') || '20');
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);
@@ -47,9 +65,11 @@ export async function GET(request: Request) {
targetDate = getToday();
}
const dailyView = await dailyService.getDailyView(targetDate);
const dailyView = await dailyService.getDailyView(
targetDate,
session.user.id
);
return NextResponse.json(dailyView);
} catch (error) {
console.error('Erreur lors de la récupération du daily:', error);
return NextResponse.json(
@@ -64,6 +84,10 @@ export async function GET(request: Request) {
*/
export async function POST(request: Request) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
}
const body = await request.json();
// Validation des données
@@ -93,17 +117,17 @@ export async function POST(request: Request) {
const checkbox = await dailyService.addCheckbox({
date,
userId: session.user.id,
text: body.text,
type: body.type,
taskId: body.taskId,
order: body.order,
isChecked: body.isChecked
isChecked: body.isChecked,
});
return NextResponse.json(checkbox, { status: 201 });
} 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(
{ error: 'Erreur interne du serveur' },
{ status: 500 }

View File

@@ -12,25 +12,24 @@ export async function GET(request: NextRequest) {
const logs = await prisma.syncLog.findMany({
where: {
source: 'jira'
source: 'jira',
},
orderBy: {
createdAt: 'desc'
createdAt: 'desc',
},
take: limit
take: limit,
});
return NextResponse.json({
data: logs
data: logs,
});
} catch (error) {
console.error('❌ Erreur récupération logs Jira:', error);
return NextResponse.json(
{
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 }
);

View File

@@ -1,7 +1,12 @@
import { NextResponse } from 'next/server';
import { createJiraService, JiraService } from '@/services/integrations/jira/jira';
import {
createJiraService,
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
@@ -10,6 +15,14 @@ import { jiraScheduler } from '@/services/integrations/jira/scheduler';
*/
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 }
);
}
// Vérifier s'il y a des actions spécifiques (scheduler)
const body = await request.json().catch(() => ({}));
const { action, ...params } = body;
@@ -19,26 +32,27 @@ export async function POST(request: Request) {
switch (action) {
case 'scheduler':
if (params.enabled) {
await jiraScheduler.start();
await jiraScheduler.start(session.user.id);
} else {
jiraScheduler.stop();
}
return NextResponse.json({
success: true,
data: await jiraScheduler.getStatus()
data: await jiraScheduler.getStatus(session.user.id),
});
case 'config':
await userPreferencesService.saveJiraSchedulerConfig(
session.user.id,
params.jiraAutoSync,
params.jiraSyncInterval
);
// Redémarrer le scheduler si la config a changé
await jiraScheduler.restart();
await jiraScheduler.restart(session.user.id);
return NextResponse.json({
success: true,
message: 'Configuration scheduler mise à jour',
data: await jiraScheduler.getStatus()
data: await jiraScheduler.getStatus(session.user.id),
});
default:
@@ -50,11 +64,18 @@ export async function POST(request: Request) {
}
// Synchronisation normale (manuelle)
const jiraConfig = await userPreferencesService.getJiraConfig();
const jiraConfig = await userPreferencesService.getJiraConfig(
session.user.id
);
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
jiraService = new JiraService({
enabled: jiraConfig.enabled,
@@ -62,7 +83,7 @@ export async function POST(request: Request) {
email: jiraConfig.email,
apiToken: jiraConfig.apiToken,
projectKey: jiraConfig.projectKey,
ignoredProjects: jiraConfig.ignoredProjects || []
ignoredProjects: jiraConfig.ignoredProjects || [],
});
} else {
// Fallback sur les variables d'environnement
@@ -71,7 +92,10 @@ export async function POST(request: Request) {
if (!jiraService) {
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 }
);
}
@@ -82,36 +106,62 @@ export async function POST(request: Request) {
const connectionOk = await jiraService.testConnection();
if (!connectionOk) {
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 }
);
}
// 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({
message: 'Synchronisation Jira terminée avec succès',
data: result
data: jiraSyncResult,
});
} else {
return NextResponse.json(
{
error: 'Synchronisation Jira terminée avec des erreurs',
data: result
data: jiraSyncResult,
},
{ status: 207 } // Multi-Status
);
}
} catch (error) {
console.error('❌ Erreur API sync Jira:', error);
return NextResponse.json(
{
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 }
);
@@ -124,12 +174,27 @@ export async function POST(request: Request) {
*/
export async function GET() {
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
const jiraConfig = await userPreferencesService.getJiraConfig();
const jiraConfig = await userPreferencesService.getJiraConfig(
session.user.id
);
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
jiraService = new JiraService({
enabled: jiraConfig.enabled,
@@ -137,7 +202,7 @@ export async function GET() {
email: jiraConfig.email,
apiToken: jiraConfig.apiToken,
projectKey: jiraConfig.projectKey,
ignoredProjects: jiraConfig.ignoredProjects || []
ignoredProjects: jiraConfig.ignoredProjects || [],
});
} else {
// Fallback sur les variables d'environnement
@@ -145,12 +210,10 @@ export async function GET() {
}
if (!jiraService) {
return NextResponse.json(
{
return NextResponse.json({
connected: false,
message: 'Configuration Jira manquante'
}
);
message: 'Configuration Jira manquante',
});
}
const connected = await jiraService.testConnection();
@@ -158,33 +221,36 @@ export async function GET() {
// Si connexion OK et qu'un projet est configuré, tester aussi le projet
let projectValidation = null;
if (connected && jiraConfig.projectKey) {
projectValidation = await jiraService.validateProject(jiraConfig.projectKey);
projectValidation = await jiraService.validateProject(
jiraConfig.projectKey
);
}
// Récupérer aussi le statut du scheduler
const schedulerStatus = await jiraScheduler.getStatus();
// Récupérer aussi le statut du scheduler avec l'utilisateur connecté
const schedulerStatus = await jiraScheduler.getStatus(session.user.id);
return NextResponse.json({
connected,
message: connected ? 'Connexion Jira OK' : 'Impossible de se connecter à Jira',
project: projectValidation ? {
message: connected
? 'Connexion Jira OK'
: 'Impossible de se connecter à Jira',
project: projectValidation
? {
key: jiraConfig.projectKey,
exists: projectValidation.exists,
name: projectValidation.name,
error: projectValidation.error
} : null,
scheduler: schedulerStatus
error: projectValidation.error,
}
: null,
scheduler: schedulerStatus,
});
} catch (error) {
console.error('❌ Erreur test connexion Jira:', error);
return NextResponse.json(
{
return NextResponse.json({
connected: false,
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 { createJiraService } from '@/services/integrations/jira/jira';
import { userPreferencesService } from '@/services/core/user-preferences';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
/**
* POST /api/jira/validate-project
@@ -8,6 +10,11 @@ import { userPreferencesService } from '@/services/core/user-preferences';
*/
export async function POST(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
}
const body = await request.json();
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
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(
{ error: 'Configuration Jira manquante. Configurez Jira dans les paramètres.' },
{
error:
'Configuration Jira manquante. Configurez Jira dans les paramètres.',
},
{ status: 400 }
);
}
@@ -32,37 +49,44 @@ export async function POST(request: NextRequest) {
const jiraService = createJiraService();
if (!jiraService) {
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 }
);
}
// Valider le projet
const validation = await jiraService.validateProject(projectKey.trim().toUpperCase());
const validation = await jiraService.validateProject(
projectKey.trim().toUpperCase()
);
if (validation.exists) {
return NextResponse.json({
success: true,
exists: true,
projectName: validation.name,
message: `Projet "${projectKey}" trouvé : ${validation.name}`
message: `Projet "${projectKey}" trouvé : ${validation.name}`,
});
} else {
return NextResponse.json({
return NextResponse.json(
{
success: false,
exists: false,
error: validation.error,
message: validation.error || `Projet "${projectKey}" introuvable`
}, { status: 404 });
message: validation.error || `Projet "${projectKey}" introuvable`,
},
{ status: 404 }
);
}
} catch (error) {
console.error('Erreur lors de la validation du projet Jira:', error);
return NextResponse.json(
{
success: false,
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 }
);

View File

@@ -0,0 +1,118 @@
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 resolvedParams = await params;
const body = await request.json();
const { title, content, taskId, folderId, isFavorite, tags } = body;
const note = await notesService.updateNote(
resolvedParams.id,
session.user.id,
{
title,
content,
taskId,
folderId,
isFavorite,
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,58 @@
import { NextRequest, NextResponse } from 'next/server';
import { readFile } from 'fs/promises';
import { join } from 'path';
import { existsSync } from 'fs';
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ filename: string }> }
) {
try {
const { filename } = await params;
// Sécurité : empêcher les path traversal attacks
if (filename.includes('..') || filename.includes('/')) {
return NextResponse.json(
{ error: 'Nom de fichier invalide' },
{ status: 400 }
);
}
// Chemin vers l'image dans data/uploads/notes
const imagePath = join(process.cwd(), 'data', 'uploads', 'notes', filename);
// Vérifier que le fichier existe
if (!existsSync(imagePath)) {
return NextResponse.json({ error: 'Image non trouvée' }, { status: 404 });
}
// Lire le fichier
const imageBuffer = await readFile(imagePath);
// Déterminer le type MIME basé sur l'extension
const extension = filename.split('.').pop()?.toLowerCase();
const mimeTypes: Record<string, string> = {
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
png: 'image/png',
gif: 'image/gif',
webp: 'image/webp',
svg: 'image/svg+xml',
};
const contentType = mimeTypes[extension || ''] || 'image/jpeg';
// Retourner l'image avec les bons headers
return new NextResponse(new Uint8Array(imageBuffer), {
headers: {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=31536000, immutable',
},
});
} catch (error) {
console.error('Error serving image:', error);
return NextResponse.json(
{ error: "Erreur lors de la récupération de l'image" },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,72 @@
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { writeFile, mkdir } from 'fs/promises';
import { join } from 'path';
import { existsSync } from 'fs';
export async function POST(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.email) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
}
const formData = await request.formData();
const file = formData.get('file') as File;
if (!file) {
return NextResponse.json(
{ error: 'Aucun fichier fourni' },
{ status: 400 }
);
}
// Vérifier que c'est bien une image
if (!file.type.startsWith('image/')) {
return NextResponse.json(
{ error: 'Le fichier doit être une image' },
{ status: 400 }
);
}
// Limiter la taille à 10MB
const maxSize = 10 * 1024 * 1024; // 10MB
if (file.size > maxSize) {
return NextResponse.json(
{ error: "L'image est trop grande (max 10MB)" },
{ status: 400 }
);
}
// Créer le dossier de stockage s'il n'existe pas
// Utiliser data/ qui est monté en volume dans Docker
const uploadsDir = join(process.cwd(), 'data', 'uploads', 'notes');
if (!existsSync(uploadsDir)) {
await mkdir(uploadsDir, { recursive: true });
}
// Générer un nom de fichier unique
const timestamp = Date.now();
const randomStr = Math.random().toString(36).substring(2, 15);
const extension = file.name.split('.').pop() || 'png';
const filename = `${timestamp}-${randomStr}.${extension}`;
const filepath = join(uploadsDir, filename);
// Convertir le File en Buffer et l'écrire
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
await writeFile(filepath, buffer);
// Retourner l'URL relative de l'image
const imageUrl = `/api/notes/images/${filename}`;
return NextResponse.json({ url: imageUrl });
} catch (error) {
console.error('Error uploading image:', error);
return NextResponse.json(
{ error: "Erreur lors de l'upload de l'image" },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,75 @@
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, folderId, 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,
folderId,
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 { 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
@@ -9,27 +11,29 @@ export async function GET(
{ params }: { params: Promise<{ id: string }> }
) {
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 tag = await tagsService.getTagById(id);
const tag = await tagsService.getTagById(id, session.user.id);
if (!tag) {
return NextResponse.json(
{ error: 'Tag non trouvé' },
{ status: 404 }
);
return NextResponse.json({ error: 'Tag non trouvé' }, { status: 404 });
}
return NextResponse.json({
data: tag,
message: 'Tag récupéré avec succès'
message: 'Tag récupéré avec succès',
});
} catch (error) {
console.error('Erreur lors de la récupération du tag:', error);
return NextResponse.json(
{
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 }
);

View File

@@ -1,11 +1,19 @@
import { NextRequest, NextResponse } from 'next/server';
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
*/
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 query = searchParams.get('q');
const popular = searchParams.get('popular');
@@ -14,27 +22,26 @@ export async function GET(request: NextRequest) {
let tags;
if (popular === 'true') {
// Récupérer les tags les plus utilisés
tags = await tagsService.getPopularTags(limit);
// Récupérer les tags les plus utilisés pour cet utilisateur
tags = await tagsService.getPopularTags(session.user.id, limit);
} else if (query) {
// Recherche par nom (pour autocomplete)
tags = await tagsService.searchTags(query, limit);
// Recherche par nom (pour autocomplete) pour cet utilisateur
tags = await tagsService.searchTags(query, session.user.id, limit);
} else {
// Récupérer tous les tags
tags = await tagsService.getTags();
// Récupérer tous les tags de cet utilisateur
tags = await tagsService.getTags(session.user.id);
}
return NextResponse.json({
data: tags,
message: 'Tags récupérés avec succès'
message: 'Tags récupérés avec succès',
});
} catch (error) {
console.error('Erreur lors de la récupération des tags:', error);
return NextResponse.json(
{
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 }
);

View File

@@ -1,5 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { tasksService } from '@/services/task-management/tasks';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
export async function GET(
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 });
} catch (error) {

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