Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/call-video-feeds/video-player-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export const VideoPlayerModal: React.FC<VideoPlayerModalProps> = ({ isOpen, onCl
};

return (
<Modal visible={isOpen} animationType="slide" presentationStyle="fullScreen" onRequestClose={onClose}>
<Modal visible={isOpen} animationType="slide" presentationStyle="fullScreen" onRequestClose={onClose} supportedOrientations={['portrait', 'portrait-upside-down', 'landscape', 'landscape-left', 'landscape-right']}>
<Box className="flex-1 bg-black">
{/* Header */}
<Box className="flex-row items-center justify-between px-4 pb-2 pt-12">
Expand Down
2 changes: 1 addition & 1 deletion src/components/calls/call-detail-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
}

return (
<Modal visible={isKebabMenuOpen} transparent={true} animationType="slide" onRequestClose={closeMenu} testID="call-detail-actionsheet">
<Modal visible={isKebabMenuOpen} transparent={true} animationType="slide" onRequestClose={closeMenu} testID="call-detail-actionsheet" supportedOrientations={['portrait', 'portrait-upside-down', 'landscape', 'landscape-left', 'landscape-right']}>

Check warning on line 75 in src/components/calls/call-detail-menu.tsx

View workflow job for this annotation

GitHub Actions / test

Replace `·visible={isKebabMenuOpen}·transparent={true}·animationType="slide"·onRequestClose={closeMenu}·testID="call-detail-actionsheet"·supportedOrientations={['portrait',·'portrait-upside-down',·'landscape',·'landscape-left',·'landscape-right']}` with `⏎········visible={isKebabMenuOpen}⏎········transparent={true}⏎········animationType="slide"⏎········onRequestClose={closeMenu}⏎········testID="call-detail-actionsheet"⏎········supportedOrientations={['portrait',·'portrait-upside-down',·'landscape',·'landscape-left',·'landscape-right']}⏎······`
<RNPressable style={menuStyles.backdrop} onPress={closeMenu}>
<RNPressable style={menuStyles.sheet}>
<View style={menuStyles.handle} />
Expand Down
2 changes: 1 addition & 1 deletion src/components/calls/call-images-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@

return (
<>
<Modal visible={isOpen} animationType="slide" presentationStyle="pageSheet" onRequestClose={handleClose}>
<Modal visible={isOpen} animationType="slide" presentationStyle="pageSheet" onRequestClose={handleClose} supportedOrientations={['portrait', 'portrait-upside-down', 'landscape', 'landscape-left', 'landscape-right']}>

Check warning on line 420 in src/components/calls/call-images-modal.tsx

View workflow job for this annotation

GitHub Actions / test

Replace `·visible={isOpen}·animationType="slide"·presentationStyle="pageSheet"·onRequestClose={handleClose}·supportedOrientations={['portrait',·'portrait-upside-down',·'landscape',·'landscape-left',·'landscape-right']}` with `⏎········visible={isOpen}⏎········animationType="slide"⏎········presentationStyle="pageSheet"⏎········onRequestClose={handleClose}⏎········supportedOrientations={['portrait',·'portrait-upside-down',·'landscape',·'landscape-left',·'landscape-right']}⏎······`
<KeyboardProvider>
<SafeAreaView style={[styles.container, isDark && styles.containerDark]}>
{/* Header */}
Expand Down
2 changes: 1 addition & 1 deletion src/components/calls/call-notes-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ const CallNotesModal = ({ isOpen, onClose, callId }: CallNotesModalProps) => {
}

return (
<Modal visible={isOpen} animationType="slide" presentationStyle="pageSheet" onRequestClose={handleClose}>
<Modal visible={isOpen} animationType="slide" presentationStyle="pageSheet" onRequestClose={handleClose} supportedOrientations={['portrait', 'portrait-upside-down', 'landscape', 'landscape-left', 'landscape-right']}>
<KeyboardProvider>
<SafeAreaView style={[styles.container, isDark && styles.containerDark]}>
{/* Header */}
Expand Down
2 changes: 1 addition & 1 deletion src/components/calls/close-call-bottom-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
const isButtonDisabled = isLoading || isSubmitting;

return (
<Modal visible={isOpen} transparent={true} animationType="slide" onRequestClose={handleClose} testID="close-call-bottom-sheet">
<Modal visible={isOpen} transparent={true} animationType="slide" onRequestClose={handleClose} testID="close-call-bottom-sheet" supportedOrientations={['portrait', 'portrait-upside-down', 'landscape', 'landscape-left', 'landscape-right']}>

Check warning on line 102 in src/components/calls/close-call-bottom-sheet.tsx

View workflow job for this annotation

GitHub Actions / test

Replace `·visible={isOpen}·transparent={true}·animationType="slide"·onRequestClose={handleClose}·testID="close-call-bottom-sheet"·supportedOrientations={['portrait',·'portrait-upside-down',·'landscape',·'landscape-left',·'landscape-right']}` with `⏎······visible={isOpen}⏎······transparent={true}⏎······animationType="slide"⏎······onRequestClose={handleClose}⏎······testID="close-call-bottom-sheet"⏎······supportedOrientations={['portrait',·'portrait-upside-down',·'landscape',·'landscape-left',·'landscape-right']}⏎····`
<RNPressable style={styles.backdrop} onPress={handleClose}>
<RNPressable style={styles.sheet} onPress={(e) => e.stopPropagation()}>
<View style={styles.handle} />
Expand Down
21 changes: 18 additions & 3 deletions src/components/command/__tests__/add-lane-sheet.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,34 @@ describe('AddLaneSheet', () => {

fireEvent.press(getByTestId('lane-save'));

expect(onSave).toHaveBeenCalledWith('Fire Attack', CommandNodeType.Group, '#e74c3c', { minUnits: 1, maxUnits: 3, minUnitPersonnel: 2, maxTimeInRole: 30 });
expect(onSave).toHaveBeenCalledWith('Fire Attack', CommandNodeType.Group, '#e74c3c', { minUnits: 1, maxUnits: 3, minUnitPersonnel: 2, maxTimeInRole: 30, workTimeAmberMinutes: 20, workTimeRedMinutes: 40 });

unmount();
});

it('passes undefined limits when none are entered', () => {
it('passes the default work-time thresholds when no limits are entered', () => {
const onSave = jest.fn();
const { getByTestId, unmount } = render(<AddLaneSheet isOpen={true} onClose={jest.fn()} onSave={onSave} />);

fireEvent.changeText(getByTestId('lane-name-input'), 'Staging');
fireEvent.press(getByTestId('lane-save'));

expect(onSave).toHaveBeenCalledWith('Staging', CommandNodeType.Division, undefined, undefined);
expect(onSave).toHaveBeenCalledWith('Staging', CommandNodeType.Division, undefined, { workTimeAmberMinutes: 20, workTimeRedMinutes: 40 });

unmount();
});

it('saves custom work-time thresholds and omits cleared ones', () => {
const onSave = jest.fn();
const { getByTestId, unmount } = render(<AddLaneSheet isOpen={true} onClose={jest.fn()} onSave={onSave} />);

fireEvent.changeText(getByTestId('lane-name-input'), 'Rehab');
fireEvent.press(getByTestId('lane-limits-toggle'));
fireEvent.changeText(getByTestId('limit-worktime-amber'), '15');
fireEvent.changeText(getByTestId('limit-worktime-red'), '');
fireEvent.press(getByTestId('lane-save'));

expect(onSave).toHaveBeenCalledWith('Rehab', CommandNodeType.Division, undefined, { workTimeAmberMinutes: 15 });
Comment on lines +57 to +67

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Unmount the new render during cleanup.

The test destructures unmount on Line 59 but never calls it after the assertion, allowing the rendered tree to leak into later tests.

As per coding guidelines, tests must always call unmount() during cleanup.

Proposed fix
    expect(onSave).toHaveBeenCalledWith('Rehab', CommandNodeType.Division, undefined, { workTimeAmberMinutes: 15 });
+   unmount();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('saves custom work-time thresholds and omits cleared ones', () => {
const onSave = jest.fn();
const { getByTestId, unmount } = render(<AddLaneSheet isOpen={true} onClose={jest.fn()} onSave={onSave} />);
fireEvent.changeText(getByTestId('lane-name-input'), 'Rehab');
fireEvent.press(getByTestId('lane-limits-toggle'));
fireEvent.changeText(getByTestId('limit-worktime-amber'), '15');
fireEvent.changeText(getByTestId('limit-worktime-red'), '');
fireEvent.press(getByTestId('lane-save'));
expect(onSave).toHaveBeenCalledWith('Rehab', CommandNodeType.Division, undefined, { workTimeAmberMinutes: 15 });
it('saves custom work-time thresholds and omits cleared ones', () => {
const onSave = jest.fn();
const { getByTestId, unmount } = render(<AddLaneSheet isOpen={true} onClose={jest.fn()} onSave={onSave} />);
fireEvent.changeText(getByTestId('lane-name-input'), 'Rehab');
fireEvent.press(getByTestId('lane-limits-toggle'));
fireEvent.changeText(getByTestId('limit-worktime-amber'), '15');
fireEvent.changeText(getByTestId('limit-worktime-red'), '');
fireEvent.press(getByTestId('lane-save'));
expect(onSave).toHaveBeenCalledWith('Rehab', CommandNodeType.Division, undefined, { workTimeAmberMinutes: 15 });
unmount();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/command/__tests__/add-lane-sheet.test.tsx` around lines 57 -
67, Call the destructured unmount function from the render result at the end of
the test after the onSave assertion, ensuring the AddLaneSheet render is cleaned
up before subsequent tests.

Source: Coding guidelines


unmount();
});
Expand Down
12 changes: 12 additions & 0 deletions src/components/command/__tests__/work-time-light.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,16 @@ describe('WorkTimeLight', () => {
expect(workTimeColor(25)).toBe('#f59e0b');
expect(workTimeColor(45)).toBe('#ef4444');
});

it('honors per-lane fatigue thresholds', () => {
expect(workTimeColor(10, 5, 15)).toBe('#f59e0b');
expect(workTimeColor(16, 5, 15)).toBe('#ef4444');
expect(workTimeColor(4, 5, 15)).toBe('#22c55e');
});

it('treats a 0 lane threshold as disabled', () => {
expect(workTimeColor(90, 0, 0)).toBe('#22c55e');
expect(workTimeColor(90, 10, 0)).toBe('#f59e0b');
expect(workTimeColor(90, 0, 30)).toBe('#ef4444');
});
});
38 changes: 35 additions & 3 deletions src/components/command/add-lane-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,16 @@ const LANE_NAME_SUGGESTION_KEYS = [
'lane_suggestion_operations',
];

/** Optional per-lane limits: 0/undefined = no limit. */
/** Optional per-lane limits: 0/undefined = no limit. Work-time thresholds: 0/blank = that color disabled. */
export interface LaneLimits {
minUnits?: number;
maxUnits?: number;
minUnitPersonnel?: number;
maxUnitPersonnel?: number;
minTimeInRole?: number;
maxTimeInRole?: number;
workTimeAmberMinutes?: number;
workTimeRedMinutes?: number;
}

interface AddLaneSheetProps {
Expand All @@ -54,7 +56,16 @@ export const AddLaneSheet: React.FC<AddLaneSheetProps> = ({ isOpen, onClose, onS
const [nodeType, setNodeType] = useState<CommandNodeType>(CommandNodeType.Division);
const [color, setColor] = useState<string | undefined>(undefined);
const [showLimits, setShowLimits] = useState(false);
const [limits, setLimits] = useState<Record<keyof LaneLimits, string>>({ minUnits: '', maxUnits: '', minUnitPersonnel: '', maxUnitPersonnel: '', minTimeInRole: '', maxTimeInRole: '' });
const [limits, setLimits] = useState<Record<keyof LaneLimits, string>>({
minUnits: '',
maxUnits: '',
minUnitPersonnel: '',
maxUnitPersonnel: '',
minTimeInRole: '',
maxTimeInRole: '',
workTimeAmberMinutes: '20',
workTimeRedMinutes: '40',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules low

Magic number '40' is duplicated as the work-time red threshold in both the useState initializer and reset logic. Extract a named constant such as DEFAULT_WORK_TIME_RED_MINUTES = 40 and reuse it in both locations to prevent drift.

Kody rule violation: Replace magic numbers with named constants

Prompt for LLM

File src/components/command/add-lane-sheet.tsx:

Line 67:

Magic number `'40'` is duplicated as the work-time red threshold in both the `useState` initializer and reset logic. Extract a named constant such as `DEFAULT_WORK_TIME_RED_MINUTES = 40` and reuse it in both locations to prevent drift.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

});

const setLimit = useCallback((key: keyof LaneLimits, value: string) => {
setLimits((current) => ({ ...current, [key]: value.replace(/[^0-9]/g, '') }));
Expand All @@ -76,7 +87,7 @@ export const AddLaneSheet: React.FC<AddLaneSheetProps> = ({ isOpen, onClose, onS
setNodeType(CommandNodeType.Division);
setColor(undefined);
setShowLimits(false);
setLimits({ minUnits: '', maxUnits: '', minUnitPersonnel: '', maxUnitPersonnel: '', minTimeInRole: '', maxTimeInRole: '' });
setLimits({ minUnits: '', maxUnits: '', minUnitPersonnel: '', maxUnitPersonnel: '', minTimeInRole: '', maxTimeInRole: '', workTimeAmberMinutes: '20', workTimeRedMinutes: '40' });
onClose();
}, [name, nodeType, color, limits, onSave, onClose]);

Expand Down Expand Up @@ -180,6 +191,27 @@ export const AddLaneSheet: React.FC<AddLaneSheetProps> = ({ isOpen, onClose, onS
</VStack>
</HStack>
<Text className="text-xs text-gray-500 dark:text-gray-400">{t('command.limit_time_help')}</Text>
<HStack space="sm">
<VStack className="flex-1" space="xs">
<Text className="text-xs font-medium text-gray-600 dark:text-gray-300">{t('command.limit_worktime_amber')}</Text>
<Input size="sm" variant="outline">
<InputField
keyboardType="number-pad"
placeholder={t('command.limit_none')}
value={limits.workTimeAmberMinutes}
onChangeText={(v) => setLimit('workTimeAmberMinutes', v)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules high

Inline arrow functions in JSX props create new function instances on every render, impacting performance. Move these handlers outside the render method or memoize them.

Kody rule violation: Avoid using .bind() or arrow functions in JSX props

Prompt for LLM

File src/components/command/add-lane-sheet.tsx:

Line 202:

Inline arrow functions in JSX props create new function instances on every render, impacting performance. Move these handlers outside the render method or memoize them.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

testID="limit-worktime-amber"
/>
</Input>
</VStack>
<VStack className="flex-1" space="xs">
<Text className="text-xs font-medium text-gray-600 dark:text-gray-300">{t('command.limit_worktime_red')}</Text>
<Input size="sm" variant="outline">
<InputField keyboardType="number-pad" placeholder={t('command.limit_none')} value={limits.workTimeRedMinutes} onChangeText={(v) => setLimit('workTimeRedMinutes', v)} testID="limit-worktime-red" />
</Input>
</VStack>
</HStack>
<Text className="text-xs text-gray-500 dark:text-gray-400">{t('command.limit_worktime_help')}</Text>
Comment on lines +194 to +214

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files matching add-lane-sheet:"
fd -a 'add-lane-sheet\.tsx$' . || true

if [ -f src/components/command/add-lane-sheet.tsx ]; then
  echo
  echo "Outline:"
  ast-grep outline src/components/command/add-lane-sheet.tsx || true
  echo
  echo "Relevant lines 160-230:"
  sed -n '160,230p' src/components/command/add-lane-sheet.tsx | nl -ba -v160
  echo
  echo "Imports/labels pattern:"
  rg -n "InputField|accessibilityLabel|accessibilityLabelledBy|limitWorktimeAmber|workTimeAmberMinutes|limit_worktime" src/components/command/add-lane-sheet.tsx
fi

Repository: Resgrid/IC

Length of output: 895


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Imports and relevant input/limit sections:"
sed -n '1,45p' src/components/command/add-lane-sheet.tsx
printf '\n---\n'
sed -n '170,230p' src/components/command/add-lane-sheet.tsx

echo
echo "Search for gluestack accessibilityLabel APIs nearby:"
rg -n "InputField|accessibilityLabel|accessibilityLabelledBy|limit_worktime|workTimeAmberMinutes|workTimeRedMinutes" src/components src/components/command/add-lane-sheet.tsx || true

Repository: Resgrid/IC

Length of output: 25660


Add accessible labels to the work-time threshold inputs.

The nearby <Text> labels are not programmatic labels for the native text fields, so screen readers can expose both inputs only via identical placeholders. Add accessibilityLabel to both <InputField> elements using t('command.limit_worktime_amber') and t('command.limit_worktime_red').

[low_effort_and_highreward]

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/command/add-lane-sheet.tsx` around lines 194 - 214, Add
accessibilityLabel to the workTimeAmberMinutes and workTimeRedMinutes InputField
elements, using the corresponding translated threshold labels from
command.limit_worktime_amber and command.limit_worktime_red. Preserve the
existing values, handlers, placeholders, and test IDs.

Source: Coding guidelines

</VStack>
) : null}
</VStack>
Expand Down
2 changes: 1 addition & 1 deletion src/components/command/command-details-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@
</VStack>

{/* Drop-a-pin picker rendered as a stacked modal so it covers the sheet full-screen */}
<Modal visible={pickerTarget !== null} animationType="slide" onRequestClose={() => setPickerTarget(null)}>
<Modal visible={pickerTarget !== null} animationType="slide" onRequestClose={() => setPickerTarget(null)} supportedOrientations={['portrait', 'portrait-upside-down', 'landscape', 'landscape-left', 'landscape-right']}>

Check warning on line 254 in src/components/command/command-details-sheet.tsx

View workflow job for this annotation

GitHub Actions / test

Replace `·visible={pickerTarget·!==·null}·animationType="slide"·onRequestClose={()·=>·setPickerTarget(null)}·supportedOrientations={['portrait',·'portrait-upside-down',·'landscape',·'landscape-left',·'landscape-right']}` with `⏎········visible={pickerTarget·!==·null}⏎········animationType="slide"⏎········onRequestClose={()·=>·setPickerTarget(null)}⏎········supportedOrientations={['portrait',·'portrait-upside-down',·'landscape',·'landscape-left',·'landscape-right']}⏎······`
<View style={{ flex: 1 }}>{pickerTarget !== null ? <FullScreenLocationPicker initialLocation={pickerInitial()} onLocationSelected={handleLocationPicked} onClose={() => setPickerTarget(null)} /> : null}</View>
</Modal>
</CustomBottomSheet>
Expand Down
30 changes: 22 additions & 8 deletions src/components/command/landscape-structure-board.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,25 @@
/** Two-line clamp: numberOfLines leaks to the DOM through the styling pipeline on web, so use CSS line-clamp there. */
const twoLine = isWeb ? {} : ({ numberOfLines: 2 } as const);

/** Work-time light thresholds (Tablet Command-style crew fatigue): green under 20m, amber under 40m, red past that. */
const WORK_TIME_AMBER_MINUTES = 20;
const WORK_TIME_RED_MINUTES = 40;
/** Default work-time light thresholds (Tablet Command-style crew fatigue): green under 20m, amber under 40m, red past that. Lanes can override; 0 disables that color. */
export const DEFAULT_WORK_TIME_AMBER_MINUTES = 20;
export const DEFAULT_WORK_TIME_RED_MINUTES = 40;

export const workTimeColor = (minutes: number) => (minutes < WORK_TIME_AMBER_MINUTES ? '#22c55e' : minutes < WORK_TIME_RED_MINUTES ? '#f59e0b' : '#ef4444');
export const workTimeColor = (minutes: number, amberAfterMinutes: number = DEFAULT_WORK_TIME_AMBER_MINUTES, redAfterMinutes: number = DEFAULT_WORK_TIME_RED_MINUTES) =>
redAfterMinutes > 0 && minutes >= redAfterMinutes ? '#ef4444' : amberAfterMinutes > 0 && minutes >= amberAfterMinutes ? '#f59e0b' : '#22c55e';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules low

Hex color literals '#ef4444', '#f59e0b', and '#22c55e' are inlined in multiple locations within the file, risking palette drift. Define them once as named constants such as WORK_TIME_RED, WORK_TIME_AMBER, and WORK_TIME_GREEN and reference those everywhere.

Kody rule violation: Centralize string constants

Prompt for LLM

File src/components/command/landscape-structure-board.tsx:

Line 34:

Hex color literals `'#ef4444'`, `'#f59e0b'`, and `'#22c55e'` are inlined in multiple locations within the file, risking palette drift. Define them once as named constants such as `WORK_TIME_RED`, `WORK_TIME_AMBER`, and `WORK_TIME_GREEN` and reference those everywhere.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.


/**
* Elapsed minutes since a resource was assigned, ticking once a minute. When the lane sets a
* MaxTimeInRole, exceeding it turns the light red and flags the resource as rotation-due.
* Amber/red thresholds come from the lane (0 = that color disabled; unset = 20/40 defaults).
*/
export const WorkTimeLight: React.FC<{ assignedOn?: string | null; rotationAfterMinutes?: number; testID?: string }> = ({ assignedOn, rotationAfterMinutes, testID }) => {
export const WorkTimeLight: React.FC<{ assignedOn?: string | null; rotationAfterMinutes?: number; amberAfterMinutes?: number; redAfterMinutes?: number; testID?: string }> = ({
assignedOn,
rotationAfterMinutes,
amberAfterMinutes = DEFAULT_WORK_TIME_AMBER_MINUTES,
redAfterMinutes = DEFAULT_WORK_TIME_RED_MINUTES,
testID,
}) => {
const { t } = useTranslation();
const [nowMs, setNowMs] = useState(() => Date.now());

Expand All @@ -57,7 +65,7 @@

return (
<HStack className="items-center" space="xs" testID={testID}>
<NativeView style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: isRotationDue ? '#ef4444' : workTimeColor(minutes) }} />
<NativeView style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: isRotationDue ? '#ef4444' : workTimeColor(minutes, amberAfterMinutes, redAfterMinutes) }} />
<Text className={`text-xs tabular-nums ${isRotationDue ? 'font-semibold text-error-600 dark:text-error-400' : 'text-gray-500 dark:text-gray-400'}`}>{`${minutes}m`}</Text>
{isRotationDue ? (
<Badge action="error" variant="solid" testID={testID ? `${testID}-rotation` : undefined}>
Expand Down Expand Up @@ -91,6 +99,10 @@
name: string;
/** Lane MaxTimeInRole (minutes) — flags the resource rotation-due when exceeded. */
rotationAfterMinutes?: number;
/** Lane work-time amber threshold (0 = amber disabled; unset = 20). */
amberAfterMinutes?: number;
/** Lane work-time red threshold (0 = red disabled; unset = 40). */
redAfterMinutes?: number;
isSelected: boolean;
onSelect: (assignmentId: string) => void;
onDragStart: (assignmentId: string) => void;
Expand All @@ -107,12 +119,12 @@
height: number;
}

const DraggableResourceCard: React.FC<DraggableResourceCardProps> = React.memo(({ assignment, name, rotationAfterMinutes, isSelected, onSelect, onDragStart, onDragEnd, onDrop, onView }) => {
const DraggableResourceCard: React.FC<DraggableResourceCardProps> = React.memo(({ assignment, name, rotationAfterMinutes, amberAfterMinutes, redAfterMinutes, isSelected, onSelect, onDragStart, onDragEnd, onDrop, onView }) => {

Check warning on line 122 in src/components/command/landscape-structure-board.tsx

View workflow job for this annotation

GitHub Actions / test

Insert `⏎··`
const { t } = useTranslation();

Check warning on line 123 in src/components/command/landscape-structure-board.tsx

View workflow job for this annotation

GitHub Actions / test

Insert `··`
const translation = useRef(new Animated.ValueXY()).current;

Check warning on line 124 in src/components/command/landscape-structure-board.tsx

View workflow job for this annotation

GitHub Actions / test

Insert `··`
const dragReadyRef = useRef(false);

Check warning on line 125 in src/components/command/landscape-structure-board.tsx

View workflow job for this annotation

GitHub Actions / test

Insert `··`
const panActiveRef = useRef(false);

Check warning on line 126 in src/components/command/landscape-structure-board.tsx

View workflow job for this annotation

GitHub Actions / test

Replace `··` with `····`
const longPressTriggeredRef = useRef(false);

Check warning on line 127 in src/components/command/landscape-structure-board.tsx

View workflow job for this annotation

GitHub Actions / test

Insert `··`
const [isDragging, setIsDragging] = useState(false);

const resetDrag = useCallback(() => {
Expand Down Expand Up @@ -203,7 +215,7 @@
{name}
</Text>
</HStack>
<WorkTimeLight assignedOn={assignment.AssignedOn} rotationAfterMinutes={rotationAfterMinutes} testID={`landscape-worktime-${assignment.ResourceAssignmentId}`} />
<WorkTimeLight assignedOn={assignment.AssignedOn} rotationAfterMinutes={rotationAfterMinutes} amberAfterMinutes={amberAfterMinutes} redAfterMinutes={redAfterMinutes} testID={`landscape-worktime-${assignment.ResourceAssignmentId}`} />
<Pressable accessibilityLabel={t('command.view_details')} accessibilityRole="button" className="p-1" onPress={handleView} testID={`landscape-resource-view-${assignment.ResourceAssignmentId}`}>
<Eye className="text-gray-400" size={16} />
</Pressable>
Expand Down Expand Up @@ -398,6 +410,8 @@
key={assignment.ResourceAssignmentId}
assignment={assignment}
rotationAfterMinutes={node.MaxTimeInRole}
amberAfterMinutes={node.WorkTimeAmberMinutes}
redAfterMinutes={node.WorkTimeRedMinutes}
isSelected={selectedAssignmentId === assignment.ResourceAssignmentId}
name={resolveResourceName(assignment.ResourceKind, assignment.ResourceId)}
onDragEnd={() => setDraggingAssignmentId(null)}
Expand Down
Loading
Loading