@allpepper/task-orchestrator-tui 1.2.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,23 +1,19 @@
1
1
  import { useState, useEffect, useCallback, useMemo } from 'react';
2
2
  import { useAdapter } from '../context/adapter-context';
3
- import type { Feature, FeatureStatus } from '@allpepper/task-orchestrator';
3
+ import type { Feature, Task } from '@allpepper/task-orchestrator';
4
4
  import type { BoardFeature, FeatureBoardColumn } from '../lib/types';
5
+ import { isCompletedStatus } from '../lib/colors';
5
6
 
6
7
  /**
7
- * Feature-status columns for the feature-based Kanban board
8
+ * v2 Feature-status columns for the feature-based Kanban board
9
+ * Features: NEW → ACTIVE → READY_TO_PROD → CLOSED (+ WILL_NOT_IMPLEMENT)
8
10
  */
9
11
  export const FEATURE_KANBAN_STATUSES = [
10
- { id: 'draft', title: 'Draft', status: 'DRAFT' },
11
- { id: 'planning', title: 'Planning', status: 'PLANNING' },
12
- { id: 'in-development', title: 'In Development', status: 'IN_DEVELOPMENT' },
13
- { id: 'testing', title: 'Testing', status: 'TESTING' },
14
- { id: 'validating', title: 'Validating', status: 'VALIDATING' },
15
- { id: 'pending-review', title: 'Pending Review', status: 'PENDING_REVIEW' },
16
- { id: 'blocked', title: 'Blocked', status: 'BLOCKED' },
17
- { id: 'on-hold', title: 'On Hold', status: 'ON_HOLD' },
18
- { id: 'deployed', title: 'Deployed', status: 'DEPLOYED' },
19
- { id: 'completed', title: 'Completed', status: 'COMPLETED' },
20
- { id: 'archived', title: 'Archived', status: 'ARCHIVED' },
12
+ { id: 'new', title: 'New', status: 'NEW' },
13
+ { id: 'active', title: 'Active', status: 'ACTIVE' },
14
+ { id: 'ready-to-prod', title: 'Ready to Prod', status: 'READY_TO_PROD' },
15
+ { id: 'closed', title: 'Closed', status: 'CLOSED' },
16
+ { id: 'will-not-implement', title: 'Will Not Implement', status: 'WILL_NOT_IMPLEMENT' },
21
17
  ] as const;
22
18
 
23
19
  interface UseFeatureKanbanReturn {
@@ -30,14 +26,12 @@ interface UseFeatureKanbanReturn {
30
26
 
31
27
  /**
32
28
  * Hook for managing the feature-based Kanban board.
33
- *
34
- * Fetches features and tasks for a project, groups features by their status
35
- * into columns, and nests tasks within each feature card.
29
+ * In v2, features move through the pipeline via advance/revert.
36
30
  */
37
31
  export function useFeatureKanban(projectId: string): UseFeatureKanbanReturn {
38
32
  const { adapter } = useAdapter();
39
33
  const [features, setFeatures] = useState<Feature[]>([]);
40
- const [tasksByFeature, setTasksByFeature] = useState<Map<string, import('@allpepper/task-orchestrator/src/domain/types').Task[]>>(new Map());
34
+ const [tasksByFeature, setTasksByFeature] = useState<Map<string, Task[]>>(new Map());
41
35
  const [loading, setLoading] = useState(true);
42
36
  const [error, setError] = useState<string | null>(null);
43
37
  const [refreshTrigger, setRefreshTrigger] = useState(0);
@@ -69,8 +63,7 @@ export function useFeatureKanban(projectId: string): UseFeatureKanbanReturn {
69
63
 
70
64
  setFeatures(featuresResult.data);
71
65
 
72
- // Group tasks by featureId
73
- const grouped = new Map<string, import('@allpepper/task-orchestrator/src/domain/types').Task[]>();
66
+ const grouped = new Map<string, Task[]>();
74
67
  for (const task of tasksResult.data) {
75
68
  if (task.featureId) {
76
69
  const list = grouped.get(task.featureId) || [];
@@ -97,7 +90,7 @@ export function useFeatureKanban(projectId: string): UseFeatureKanbanReturn {
97
90
  tasks,
98
91
  taskCounts: {
99
92
  total: tasks.length,
100
- completed: tasks.filter((t) => t.status === 'COMPLETED' || t.status === 'DEPLOYED').length,
93
+ completed: tasks.filter((t) => isCompletedStatus(t.status)).length,
101
94
  },
102
95
  };
103
96
  });
@@ -113,7 +106,6 @@ export function useFeatureKanban(projectId: string): UseFeatureKanbanReturn {
113
106
 
114
107
  const moveFeature = useCallback(
115
108
  async (featureId: string, newStatus: string): Promise<boolean> => {
116
- // Find feature in current data
117
109
  const feature = features.find((f) => f.id === featureId);
118
110
  if (!feature) {
119
111
  refresh();
@@ -121,20 +113,43 @@ export function useFeatureKanban(projectId: string): UseFeatureKanbanReturn {
121
113
  }
122
114
 
123
115
  try {
124
- const result = await adapter.setFeatureStatus(
125
- featureId,
126
- newStatus as FeatureStatus,
127
- feature.version
128
- );
116
+ // Determine direction based on pipeline position
117
+ const currentIdx = FEATURE_KANBAN_STATUSES.findIndex(s => s.status === feature.status);
118
+ const targetIdx = FEATURE_KANBAN_STATUSES.findIndex(s => s.status === newStatus);
129
119
 
130
- if (result.success) {
131
- refresh();
132
- return true;
133
- } else {
120
+ if (currentIdx === -1 || targetIdx === -1) {
134
121
  refresh();
135
122
  return false;
136
123
  }
124
+
125
+ // Handle terminate (WILL_NOT_IMPLEMENT)
126
+ if (newStatus === 'WILL_NOT_IMPLEMENT') {
127
+ const result = await adapter.terminate('feature', featureId, feature.version);
128
+ refresh();
129
+ return result.success;
130
+ }
131
+
132
+ // Step through the pipeline
133
+ let currentVersion = feature.version;
134
+ const direction = targetIdx > currentIdx ? 'advance' : 'revert';
135
+ const steps = Math.abs(targetIdx - currentIdx);
136
+
137
+ for (let i = 0; i < steps; i++) {
138
+ const result = direction === 'advance'
139
+ ? await adapter.advance('feature', featureId, currentVersion)
140
+ : await adapter.revert('feature', featureId, currentVersion);
141
+
142
+ if (!result.success) {
143
+ refresh();
144
+ return false;
145
+ }
146
+ currentVersion = result.data.entity.version;
147
+ }
148
+
149
+ refresh();
150
+ return true;
137
151
  } catch (_err) {
152
+ refresh();
138
153
  return false;
139
154
  }
140
155
  },
@@ -5,14 +5,14 @@ import type { BoardColumn } from '../lib/types';
5
5
  import { useBoardData } from './use-data';
6
6
 
7
7
  /**
8
- * Standard Kanban column definitions
8
+ * v2 Kanban column definitions (pipeline states)
9
9
  */
10
10
  const KANBAN_STATUSES = [
11
- { id: 'pending', title: 'Pending', status: 'PENDING' },
12
- { id: 'in-progress', title: 'In Progress', status: 'IN_PROGRESS' },
13
- { id: 'in-review', title: 'In Review', status: 'IN_REVIEW' },
14
- { id: 'blocked', title: 'Blocked', status: 'BLOCKED' },
15
- { id: 'completed', title: 'Completed', status: 'COMPLETED' },
11
+ { id: 'new', title: 'New', status: 'NEW' },
12
+ { id: 'active', title: 'Active', status: 'ACTIVE' },
13
+ { id: 'to-be-tested', title: 'To Be Tested', status: 'TO_BE_TESTED' },
14
+ { id: 'ready-to-prod', title: 'Ready to Prod', status: 'READY_TO_PROD' },
15
+ { id: 'closed', title: 'Closed', status: 'CLOSED' },
16
16
  ] as const;
17
17
 
18
18
  interface UseKanbanReturn {
@@ -24,13 +24,9 @@ interface UseKanbanReturn {
24
24
  }
25
25
 
26
26
  /**
27
- * Hook for managing Kanban board state
28
- *
29
- * Fetches tasks for a project and organizes them into Kanban columns by status.
30
- * Provides functionality to move tasks between columns with optimistic updates.
31
- *
32
- * @param projectId - The project ID to fetch tasks for
33
- * @returns Kanban board state and operations
27
+ * Hook for managing Kanban board state.
28
+ * In v2, tasks move through the pipeline via advance/revert.
29
+ * The moveTask function uses advance for forward moves and revert for backward moves.
34
30
  */
35
31
  export function useKanban(projectId: string): UseKanbanReturn {
36
32
  const { adapter } = useAdapter();
@@ -46,15 +42,10 @@ export function useKanban(projectId: string): UseKanbanReturn {
46
42
  ), [columnsByStatus]);
47
43
 
48
44
  /**
49
- * Move a task to a new status
50
- *
51
- * @param taskId - ID of the task to move
52
- * @param newStatus - New status to assign
53
- * @returns true if successful, false otherwise
45
+ * Move a task to a new status via advance/revert pipeline operations
54
46
  */
55
47
  const moveTask = useCallback(
56
48
  async (taskId: string, newStatus: string): Promise<boolean> => {
57
- // Find the task in current columns
58
49
  let task: Task | undefined;
59
50
  for (const column of columns) {
60
51
  task = column.tasks.find((t) => t.id === taskId);
@@ -62,24 +53,41 @@ export function useKanban(projectId: string): UseKanbanReturn {
62
53
  }
63
54
 
64
55
  if (!task) {
65
- // Board may be stale; ask the caller to refresh and retry.
66
56
  refresh();
67
57
  return false;
68
58
  }
69
59
 
70
60
  try {
71
- // Call adapter to update task status
72
- const result = await adapter.setTaskStatus(taskId, newStatus as any, task.version);
61
+ // Determine direction based on pipeline position
62
+ const currentIdx = KANBAN_STATUSES.findIndex(s => s.status === task!.status);
63
+ const targetIdx = KANBAN_STATUSES.findIndex(s => s.status === newStatus);
73
64
 
74
- if (result.success) {
75
- // Refresh the board on success
76
- refresh();
77
- return true;
78
- } else {
65
+ if (currentIdx === -1 || targetIdx === -1) {
79
66
  refresh();
80
67
  return false;
81
68
  }
69
+
70
+ // Step through the pipeline one step at a time
71
+ let currentVersion = task.version;
72
+ const direction = targetIdx > currentIdx ? 'advance' : 'revert';
73
+ const steps = Math.abs(targetIdx - currentIdx);
74
+
75
+ for (let i = 0; i < steps; i++) {
76
+ const result = direction === 'advance'
77
+ ? await adapter.advance('task', taskId, currentVersion)
78
+ : await adapter.revert('task', taskId, currentVersion);
79
+
80
+ if (!result.success) {
81
+ refresh();
82
+ return false;
83
+ }
84
+ currentVersion = result.data.entity.version;
85
+ }
86
+
87
+ refresh();
88
+ return true;
82
89
  } catch (_err) {
90
+ refresh();
83
91
  return false;
84
92
  }
85
93
  },
package/src/ui/index.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  export { darkTheme } from './themes/dark';
9
9
  export { lightTheme } from './themes/light';
10
10
  export type { Theme, TaskCounts, StatusKey } from './themes/types';
11
+ export type { WorkflowState, TransitionResult } from './adapters/types';
11
12
 
12
13
  // Lib - Types
13
14
  export {
@@ -1,9 +1,16 @@
1
+ /**
2
+ * Color and Status Utilities
3
+ *
4
+ * Theme-aware color helpers for status badges, priority indicators, etc.
5
+ * Updated for v2 pipeline states.
6
+ */
7
+
1
8
  import type { Theme, StatusKey } from '../themes/types';
2
9
  import type { Priority } from '@allpepper/task-orchestrator';
3
10
 
4
11
  /**
5
- * Get the color for a status value
6
- * Falls back to muted color if status not found
12
+ * Get the color for a status value.
13
+ * Falls back to muted color if status not found.
7
14
  */
8
15
  export function getStatusColor(status: string, theme: Theme): string {
9
16
  const color = theme.colors.status[status as StatusKey];
@@ -19,9 +26,6 @@ export function getPriorityColor(priority: Priority, theme: Theme): string {
19
26
 
20
27
  /**
21
28
  * Get priority dots visual indicator
22
- * HIGH: ●●● (3 filled)
23
- * MEDIUM: ●●○ (2 filled)
24
- * LOW: ●○○ (1 filled)
25
29
  */
26
30
  export function getPriorityDots(priority: Priority): string {
27
31
  switch (priority) {
@@ -47,33 +51,32 @@ export function getSemanticColor(
47
51
  }
48
52
 
49
53
  /**
50
- * Determine if a status represents an "active" state
54
+ * Check if a status represents an active/in-progress state
51
55
  */
52
56
  export function isActiveStatus(status: string): boolean {
53
- const activeStatuses = [
54
- 'IN_PROGRESS',
55
- 'IN_DEVELOPMENT',
56
- 'IN_REVIEW',
57
- 'TESTING',
58
- 'VALIDATING',
59
- 'INVESTIGATING',
60
- 'READY_FOR_QA',
61
- ];
62
- return activeStatuses.includes(status);
57
+ return status === 'ACTIVE' || status === 'TO_BE_TESTED' || status === 'READY_TO_PROD';
63
58
  }
64
59
 
65
60
  /**
66
- * Determine if a status represents a "blocked" state
61
+ * Check if a status represents a terminal/completed state
67
62
  */
68
- export function isBlockedStatus(status: string): boolean {
69
- const blockedStatuses = ['BLOCKED', 'ON_HOLD', 'CHANGES_REQUESTED'];
70
- return blockedStatuses.includes(status);
63
+ export function isCompletedStatus(status: string): boolean {
64
+ return status === 'CLOSED' || status === 'WILL_NOT_IMPLEMENT';
71
65
  }
72
66
 
73
67
  /**
74
- * Determine if a status represents a "completed" state
68
+ * Check if a status is the initial state
75
69
  */
76
- export function isCompletedStatus(status: string): boolean {
77
- const completedStatuses = ['COMPLETED', 'DEPLOYED', 'ARCHIVED', 'CANCELLED'];
78
- return completedStatuses.includes(status);
70
+ export function isNewStatus(status: string): boolean {
71
+ return status === 'NEW';
72
+ }
73
+
74
+ /**
75
+ * In v2, blocking is field-based (blockedBy array), not a status.
76
+ * This function always returns false since no status is "blocked".
77
+ * Use the blockedBy field on the entity instead.
78
+ * @deprecated Use entity.blockedBy.length > 0 instead
79
+ */
80
+ export function isBlockedStatus(_status: string): boolean {
81
+ return false;
79
82
  }
@@ -64,7 +64,7 @@ function MarkdownLine({ line }: MarkdownLineProps) {
64
64
 
65
65
  // Header (## Header)
66
66
  const headerMatch = line.match(/^(#{1,6})\s+(.*)$/);
67
- if (headerMatch) {
67
+ if (headerMatch && headerMatch[1] && headerMatch[2] !== undefined) {
68
68
  const level = headerMatch[1].length;
69
69
  const text = headerMatch[2];
70
70
  return (
@@ -78,7 +78,7 @@ function MarkdownLine({ line }: MarkdownLineProps) {
78
78
 
79
79
  // Bullet point (- item or * item)
80
80
  const bulletMatch = line.match(/^(\s*)([-*])\s+(.*)$/);
81
- if (bulletMatch) {
81
+ if (bulletMatch && bulletMatch[1] !== undefined && bulletMatch[3] !== undefined) {
82
82
  const indent = bulletMatch[1].length;
83
83
  const content = bulletMatch[3];
84
84
  return (
@@ -114,7 +114,7 @@ function InlineMarkdown({ text }: InlineMarkdownProps) {
114
114
  while (remaining.length > 0) {
115
115
  // Check for inline code `code`
116
116
  const codeMatch = remaining.match(/^(.*?)`([^`]+)`(.*)$/);
117
- if (codeMatch) {
117
+ if (codeMatch && codeMatch[2] !== undefined && codeMatch[3] !== undefined) {
118
118
  if (codeMatch[1]) {
119
119
  parts.push(<BoldMarkdown key={key++} text={codeMatch[1]} />);
120
120
  }
@@ -129,7 +129,7 @@ function InlineMarkdown({ text }: InlineMarkdownProps) {
129
129
 
130
130
  // Check for bold **text**
131
131
  const boldMatch = remaining.match(/^(.*?)\*\*([^*]+)\*\*(.*)$/);
132
- if (boldMatch) {
132
+ if (boldMatch && boldMatch[2] !== undefined && boldMatch[3] !== undefined) {
133
133
  if (boldMatch[1]) {
134
134
  parts.push(<Text key={key++}>{boldMatch[1]}</Text>);
135
135
  }
@@ -160,7 +160,7 @@ function BoldMarkdown({ text }: { text: string }) {
160
160
 
161
161
  while (remaining.length > 0) {
162
162
  const boldMatch = remaining.match(/^(.*?)\*\*([^*]+)\*\*(.*)$/);
163
- if (boldMatch) {
163
+ if (boldMatch && boldMatch[2] !== undefined && boldMatch[3] !== undefined) {
164
164
  if (boldMatch[1]) {
165
165
  parts.push(<Text key={key++}>{boldMatch[1]}</Text>);
166
166
  }
@@ -113,7 +113,6 @@ export interface ProjectOverview {
113
113
  id: string;
114
114
  name: string;
115
115
  summary: string;
116
- status: string;
117
116
  };
118
117
  taskCounts: {
119
118
  total: number;
@@ -10,37 +10,19 @@ export const darkTheme: Theme = {
10
10
  muted: '#6b6b8d',
11
11
  border: '#3d3d5c',
12
12
 
13
- // Status colors (unique keys only)
13
+ // Status colors (v2 pipeline states)
14
14
  status: {
15
- // Project/Feature/Task shared
16
- PLANNING: '#a0a0c0',
17
- IN_DEVELOPMENT: '#4da6ff',
18
- ON_HOLD: '#cc9966',
19
- CANCELLED: '#808080',
20
- COMPLETED: '#4ade80',
21
- ARCHIVED: '#555555',
22
-
23
- // Feature/Task shared
24
- TESTING: '#66cccc',
25
- BLOCKED: '#ff6666',
26
- DEPLOYED: '#34d399',
27
-
28
- // Feature only
29
- DRAFT: '#6b6b8d',
30
- VALIDATING: '#66cc99',
31
- PENDING_REVIEW: '#b366ff',
32
-
33
- // Task only
34
- BACKLOG: '#6b6b8d',
35
- PENDING: '#a0a0c0',
36
- IN_PROGRESS: '#4da6ff',
37
- IN_REVIEW: '#b366ff',
38
- CHANGES_REQUESTED: '#ff9966',
39
- READY_FOR_QA: '#66cc99',
40
- INVESTIGATING: '#ffcc66',
41
- DEFERRED: '#999999',
15
+ NEW: '#a0a0c0',
16
+ ACTIVE: '#4da6ff',
17
+ TO_BE_TESTED: '#66cccc',
18
+ READY_TO_PROD: '#66cc99',
19
+ CLOSED: '#4ade80',
20
+ WILL_NOT_IMPLEMENT: '#808080',
42
21
  },
43
22
 
23
+ // Blocked overlay color
24
+ blocked: '#ff6666',
25
+
44
26
  // Priority colors
45
27
  priority: {
46
28
  [Priority.HIGH]: '#ff6b6b',
@@ -7,57 +7,39 @@ export const lightTheme: Theme = {
7
7
  // Base colors
8
8
  background: '#ffffff',
9
9
  foreground: '#1a1a2e',
10
- muted: '#6b7280',
11
- border: '#e5e7eb',
10
+ muted: '#9090a0',
11
+ border: '#d0d0e0',
12
12
 
13
- // Status colors (unique keys only)
13
+ // Status colors (v2 pipeline states)
14
14
  status: {
15
- // Project/Feature/Task shared
16
- PLANNING: '#6b7280',
17
- IN_DEVELOPMENT: '#3b82f6',
18
- ON_HOLD: '#d97706',
19
- CANCELLED: '#6b7280',
20
- COMPLETED: '#16a34a',
21
- ARCHIVED: '#9ca3af',
22
-
23
- // Feature/Task shared
24
- TESTING: '#06b6d4',
25
- BLOCKED: '#ef4444',
26
- DEPLOYED: '#22c55e',
27
-
28
- // Feature only
29
- DRAFT: '#9ca3af',
30
- VALIDATING: '#14b8a6',
31
- PENDING_REVIEW: '#8b5cf6',
32
-
33
- // Task only
34
- BACKLOG: '#9ca3af',
35
- PENDING: '#6b7280',
36
- IN_PROGRESS: '#3b82f6',
37
- IN_REVIEW: '#8b5cf6',
38
- CHANGES_REQUESTED: '#f97316',
39
- READY_FOR_QA: '#14b8a6',
40
- INVESTIGATING: '#eab308',
41
- DEFERRED: '#9ca3af',
15
+ NEW: '#7070a0',
16
+ ACTIVE: '#2563eb',
17
+ TO_BE_TESTED: '#0891b2',
18
+ READY_TO_PROD: '#059669',
19
+ CLOSED: '#16a34a',
20
+ WILL_NOT_IMPLEMENT: '#6b7280',
42
21
  },
43
22
 
23
+ // Blocked overlay color
24
+ blocked: '#dc2626',
25
+
44
26
  // Priority colors
45
27
  priority: {
46
28
  [Priority.HIGH]: '#dc2626',
47
- [Priority.MEDIUM]: '#ca8a04',
29
+ [Priority.MEDIUM]: '#d97706',
48
30
  [Priority.LOW]: '#16a34a',
49
31
  },
50
32
 
51
33
  // Semantic colors
52
34
  accent: '#2563eb',
53
35
  success: '#16a34a',
54
- warning: '#ca8a04',
36
+ warning: '#d97706',
55
37
  error: '#dc2626',
56
38
  danger: '#dc2626',
57
39
  info: '#0891b2',
58
40
 
59
41
  // Interactive colors
60
- selection: '#dbeafe',
61
- highlight: '#bfdbfe',
42
+ selection: '#e0e0f0',
43
+ highlight: '#c0c0e0',
62
44
  },
63
45
  };
@@ -1,33 +1,18 @@
1
1
  import type { Priority } from '@allpepper/task-orchestrator';
2
2
 
3
3
  /**
4
- * All possible status values across Project, Feature, and Task
5
- * Using string literal union to avoid duplicate key issues
4
+ * v2 pipeline status keys
5
+ * Task: NEW, ACTIVE, TO_BE_TESTED, READY_TO_PROD, CLOSED, WILL_NOT_IMPLEMENT
6
+ * Feature: NEW, ACTIVE, READY_TO_PROD, CLOSED, WILL_NOT_IMPLEMENT
7
+ * Projects: stateless (no status)
6
8
  */
7
9
  export type StatusKey =
8
- // Project statuses
9
- | 'PLANNING'
10
- | 'IN_DEVELOPMENT'
11
- | 'ON_HOLD'
12
- | 'CANCELLED'
13
- | 'COMPLETED'
14
- | 'ARCHIVED'
15
- // Feature-only statuses
16
- | 'DRAFT'
17
- | 'TESTING'
18
- | 'VALIDATING'
19
- | 'PENDING_REVIEW'
20
- | 'BLOCKED'
21
- | 'DEPLOYED'
22
- // Task-only statuses
23
- | 'BACKLOG'
24
- | 'PENDING'
25
- | 'IN_PROGRESS'
26
- | 'IN_REVIEW'
27
- | 'CHANGES_REQUESTED'
28
- | 'READY_FOR_QA'
29
- | 'INVESTIGATING'
30
- | 'DEFERRED';
10
+ | 'NEW'
11
+ | 'ACTIVE'
12
+ | 'TO_BE_TESTED'
13
+ | 'READY_TO_PROD'
14
+ | 'CLOSED'
15
+ | 'WILL_NOT_IMPLEMENT';
31
16
 
32
17
  /**
33
18
  * Theme interface for the Task Orchestrator UI
@@ -42,9 +27,12 @@ export interface Theme {
42
27
  muted: string;
43
28
  border: string;
44
29
 
45
- // Status colors - maps all unique status values
30
+ // Status colors - maps v2 pipeline states
46
31
  status: Record<StatusKey, string>;
47
32
 
33
+ // Blocked overlay color (for field-based blocking indicator)
34
+ blocked: string;
35
+
48
36
  // Priority colors
49
37
  priority: Record<Priority, string>;
50
38