improvement: tasks UI responsivness improved for mobile devices.

This commit is contained in:
2026-07-13 16:49:24 +03:30
parent 08a1f34c4f
commit 547457d637
8 changed files with 95 additions and 74 deletions

View File

@@ -1,6 +1,6 @@
---
description: Lab Tasks tab — sort, grouping, filters, prosthesis colors
globs: frontend/src/components/ui/lab/TasksPage.tsx,frontend/src/components/ui/lab/Task*.tsx,frontend/src/components/ui/lab/Case*.tsx,frontend/src/components/ui/treatment/LabCasesDispatchPanel.tsx,frontend/src/components/lab/taskListGrouping.ts,frontend/src/components/lab/labCaseDueDateDisplay.ts,frontend/src/components/treatment/prosthesisTypeDisplay.ts,frontend/src/components/shared/catalog-type-colors.ts,backend/src/modules/tasks/**,backend/src/common/lab-case-due-date.ts,backend/src/modules/treatments/**
globs: frontend/src/components/ui/lab/TasksPage.tsx,frontend/src/components/ui/lab/Task*.tsx,frontend/src/components/ui/lab/Case*.tsx,frontend/src/components/ui/treatment/LabCasesDispatchPanel.tsx,frontend/src/components/lab/taskListGrouping.ts,frontend/src/components/lab/labCaseDueDateDisplay.ts,frontend/src/components/shared/formSelectStyles.ts,frontend/src/components/treatment/prosthesisTypeDisplay.ts,frontend/src/components/shared/catalog-type-colors.ts,backend/src/modules/tasks/**,backend/src/common/lab-case-due-date.ts,backend/src/modules/treatments/**
alwaysApply: false
---
@@ -12,6 +12,7 @@ alwaysApply: false
- **Important first:** `pinImportant=true` (sort pin, not filter).
- **Task assignment:** assign in Cases (`TAB_CASES_EDIT`); status edit on Tasks only for assignee or unassigned tasks; others see “Assigned to {name}”.
- **Case due dates:** clinic sets in Treatment lab dispatch; lab sees on Cases/Tasks; `overdue` filter + `sortBy=dueDate` on Tasks.
- **Mobile UX:** `LAB_TASK_STATUS_SELECT_CLASS` (44px tap target on small screens); `TaskCaseGroupHeader` sticky while scrolling grouped tasks; filter selects use same touch sizing on Tasks.
- **Show in case:** `GET /tasks/locate-page` finds page in full list; highlight + scroll.
Full map: `.cursor/skills/lab-tasks/SKILL.md`

View File

@@ -75,7 +75,16 @@ List items include `caseSentAt`, `caseDueDate`, `isCaseOverdue`, `assignee`, `as
- **Clinic set:** Treatment lab dispatch panel — date input on unsent shipment (saved with draft/send); on sent cases, blur saves via `PATCH /treatments/lab-cases/:labCaseId/due-date`.
- **Edit lock:** Clinic cannot change due date after **all** tasks are completed (`taskProgress.completed === taskProgress.total`).
- **Lab display:** Cases list + detail; Tasks case group header when grouped by date.
- **Utils:** `backend/src/common/lab-case-due-date.ts`, `frontend/src/components/lab/labCaseDueDateDisplay.ts`.
- **Utils:** `backend/src/common/lab-case-due-date.ts`, `frontend/src/components/lab/labCaseDueDateDisplay.ts`, `LabCaseDueDateBadge`.
## Mobile UX (Tasks + Treatment dispatch)
Keep changes minimal — match existing `sm:` breakpoint patterns elsewhere in the app.
- **Task status control:** `LAB_TASK_STATUS_SELECT_CLASS` in [`formSelectStyles.ts`](frontend/src/components/shared/formSelectStyles.ts) — full-width, `min-h-[44px]`, `text-base` on mobile; compact on `sm+`. Read-only status / assignee badges match height on mobile.
- **Sticky case header:** `TaskCaseGroupHeader` uses `sticky top-0 z-10` + translucent background when `sortBy=date` (grouped view). Sticks within dashboard `<main>` scroll.
- **Tasks filters:** filter `<select>`s use the same 44px mobile height as other form controls.
- **Treatment lab dispatch:** shipment card `p-3 sm:p-4`; **Send to lab** is `w-full sm:w-auto`.
## Permissions

View File

@@ -45,7 +45,7 @@ frontend/src/
- **History filters** are client-side only (`treatmentHistoryFilters.ts`): “Not shipped to lab” + single date on already-fetched patient history; includes live current draft when filtering.
- **Lab case comments** on a detail when sent and lab case tasks are not all `COMPLETED` (`taskProgress` from API).
**Lab Tasks tab:** Newest case first; steps ordered 1→N; case grouping when sorted by date; `stepCompleted` filter; prosthesis colors from `PROSTHESIS_TYPE_COLORS` via catalog; task assignment in **Cases** (compact row: status + assignee + last update); on **Tasks**, all staff see every task but only assignee (or unassigned pool) can change status — others see “Assigned to {name}” instead of the status dropdown; **case due dates** set/edited in clinic Treatment lab dispatch, shown on lab Cases/Tasks with overdue filter + sort — see `.cursor/skills/lab-tasks/SKILL.md`.
**Lab Tasks tab:** Newest case first; steps ordered 1→N; case grouping when sorted by date; `stepCompleted` filter; prosthesis colors from `PROSTHESIS_TYPE_COLORS` via catalog; task assignment in **Cases** (compact row: status + assignee + last update); on **Tasks**, all staff see every task but only assignee (or unassigned pool) can change status — others see “Assigned to {name}” instead of the status dropdown; **case due dates** set/edited in clinic Treatment lab dispatch, shown on lab Cases/Tasks with overdue filter + sort; **mobile:** larger task status controls, sticky case header when grouped — see `.cursor/skills/lab-tasks/SKILL.md`.
## Backend layout

View File

@@ -1,3 +1,5 @@
/** Shared native select styling — readable in light and dark themes */
export const FORM_SELECT_CLASS =
'form-select rounded border border-border bg-background-card text-text-primary px-2 py-1 text-sm disabled:opacity-60 focus:outline-none focus:ring-2 focus:ring-primary/35';
/** Lab task status control — larger tap target on small screens. */
export const LAB_TASK_STATUS_SELECT_CLASS = `${FORM_SELECT_CLASS} h-[44px] w-full py-2 text-base font-medium sm:h-9 sm:py-1 sm:text-sm sm:max-w-none`;

View File

@@ -28,7 +28,7 @@ export function TaskCaseGroupHeader({ caseGroup, locale }: TaskCaseGroupHeaderPr
: null;
return (
<div className="flex flex-wrap items-center justify-between gap-2 px-3 py-2.5 bg-background-secondary/60 border-b border-border/80">
<div className="sticky top-0 z-10 flex flex-wrap items-center justify-between gap-2 border-b border-border/80 bg-background-secondary/95 px-3 py-2.5 backdrop-blur-sm supports-[backdrop-filter]:bg-background-secondary/80">
<div className="min-w-0 space-y-0.5">
<div className="flex flex-wrap items-center gap-1.5">
<p className="text-sm font-semibold text-text-primary truncate">

View File

@@ -4,7 +4,7 @@ import { Check, FolderOpen, MessageSquare } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { Badge } from '@/components/ui/shared/Badge';
import { Button } from '@/components/ui/shared/Button';
import { FORM_SELECT_CLASS } from '@/components/shared/formSelectStyles';
import { LAB_TASK_STATUS_SELECT_CLASS } from '@/components/shared/formSelectStyles';
import { LabCaseCommentsPanel } from '@/components/ui/lab/LabCaseCommentsPanel';
import {
canEditLabTaskStatus,
@@ -79,12 +79,63 @@ export function TaskRow({
.filter(Boolean)
.join(' ');
const commentsButton =
canEdit && !exiting ? (
<button
type="button"
onClick={() => onToggleComments(task.id)}
className={`inline-flex h-[44px] w-[44px] shrink-0 items-center justify-center rounded-md border sm:h-9 sm:w-9 ${
commentsOpen
? 'border-primary bg-primary/10 text-primary'
: 'border-border text-text-muted hover:border-primary/40'
}`}
title={t('commentsButton')}
aria-label={t('commentsButton')}
>
<MessageSquare className="h-4 w-4" />
</button>
) : null;
const statusControl = canEditStatus && !exiting ? (
<select
value={task.status}
disabled={updatingTaskId === task.id || exiting}
onChange={(e) => onStatusUpdate(task.id, e.target.value as LabTaskStatus)}
className={LAB_TASK_STATUS_SELECT_CLASS}
style={labTaskStatusSelectStyle(task.status)}
>
{statusOptions.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
) : assignedToOther && !exiting ? (
<Badge
variant="default"
fixedWidth={false}
className="h-[44px] w-full text-sm text-center whitespace-normal leading-snug sm:h-9 sm:w-auto sm:max-w-[10.5rem] sm:text-xs"
>
{t('assignedToStaff', { name: task.assignee!.name })}
</Badge>
) : (
<Badge
variant={labTaskStatusVariant(task.status)}
fixedWidth={false}
className="h-[44px] w-full text-sm sm:h-9 sm:w-auto sm:text-xs"
>
{statusOptions.find((opt) => opt.value === task.status)?.label ?? task.status}
</Badge>
);
return (
<li id={`task-row-${task.id}`} className={rowClassName}>
<div
className={`flex flex-col gap-3 px-3 py-3 sm:grid sm:grid-cols-[minmax(0,1fr)_minmax(8.5rem,11rem)_auto] sm:items-center sm:gap-x-3 sm:gap-y-0.5 sm:py-2 ${
flatMode ? '' : 'ps-5'
}`}
className={`flex flex-col gap-3 px-3 py-3 sm:grid sm:items-center sm:gap-x-3 sm:gap-y-0.5 sm:py-2 ${
flatMode
? 'sm:grid-cols-[minmax(0,1fr)_minmax(10rem,14rem)_auto]'
: 'sm:grid-cols-[minmax(0,1fr)_minmax(10rem,14rem)]'
} ${flatMode ? '' : 'ps-3 sm:ps-5'}`}
>
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-1.5">
@@ -127,68 +178,25 @@ export function TaskRow({
</p>
</div>
<div
className={`flex sm:justify-center pe-2 sm:pe-3 shrink-0 ${
assignedToOther ? 'mb-1 sm:mb-0' : ''
}`}
>
{canEditStatus && !exiting ? (
<select
value={task.status}
disabled={updatingTaskId === task.id || exiting}
onChange={(e) => onStatusUpdate(task.id, e.target.value as LabTaskStatus)}
className={`${FORM_SELECT_CLASS} w-full sm:max-w-[132px] font-medium`}
style={labTaskStatusSelectStyle(task.status)}
>
{statusOptions.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
) : assignedToOther && !exiting ? (
<Badge
variant="default"
fixedWidth={false}
className="text-center whitespace-normal leading-snug max-w-[10.5rem]"
>
{t('assignedToStaff', { name: task.assignee!.name })}
</Badge>
) : (
<Badge variant={labTaskStatusVariant(task.status)} fixedWidth={false}>
{statusOptions.find((opt) => opt.value === task.status)?.label ?? task.status}
</Badge>
)}
<div className="flex w-full min-w-0 items-stretch gap-2">
<div className="min-w-0 flex-1">{statusControl}</div>
{commentsButton}
</div>
<div className="flex items-center gap-1.5 shrink-0 justify-between sm:justify-end flex-wrap ps-1 sm:ps-0">
{flatMode && onShowInCase && !exiting ? (
<Button
type="button"
variant="ghost"
size="sm"
className="text-xs px-2 py-1 h-auto"
onClick={() => onShowInCase(task)}
>
<FolderOpen className="h-3.5 w-3.5 me-1" />
{t('showInCase')}
</Button>
) : null}
{canEdit && !exiting ? (
<button
type="button"
onClick={() => onToggleComments(task.id)}
className={`p-1.5 rounded border ${
commentsOpen
? 'border-primary bg-primary/10 text-primary'
: 'border-border text-text-muted hover:border-primary/40'
}`}
title={t('commentsButton')}
>
<MessageSquare className="h-4 w-4" />
</button>
) : null}
{flatMode ? (
{flatMode ? (
<div className="flex items-center gap-1.5 shrink-0 justify-between sm:justify-end flex-wrap">
{onShowInCase && !exiting ? (
<Button
type="button"
variant="ghost"
size="sm"
className="text-xs px-2 py-1 h-auto"
onClick={() => onShowInCase(task)}
>
<FolderOpen className="h-3.5 w-3.5 me-1" />
{t('showInCase')}
</Button>
) : null}
<Badge
fixedWidth={false}
truncate
@@ -201,8 +209,8 @@ export function TaskRow({
>
{task.prosthesisTypeLabel}
</Badge>
) : null}
</div>
</div>
) : null}
</div>
{commentsOpen && canEdit && !exiting ? (

View File

@@ -282,7 +282,7 @@ export function TasksPage() {
[canEdit, loadTasks, setError, showError, showSuccess, statusFilter, t, tErrors],
);
const filterSelectClass = `${FORM_SELECT_CLASS} w-full rounded-md px-2 py-1.5 text-sm`;
const filterSelectClass = `${FORM_SELECT_CLASS} w-full min-h-[44px] rounded-md px-2 py-2 text-base sm:min-h-0 sm:py-1.5 sm:text-sm`;
const sortHintKey = useMemo(() => {
switch (sortBy) {
@@ -467,7 +467,7 @@ export function TasksPage() {
) : null}
</section>
<section className="surface-card min-h-[280px]">
<section className="surface-card min-h-[280px] overflow-x-hidden">
{(loading || locatingCase) && tasks.length === 0 ? (
<p className="p-3 text-sm text-text-muted">
{locatingCase ? t('locatingCase') : t('loading')}

View File

@@ -315,7 +315,7 @@ export function LabCasesDispatchPanel({
{!detailAlreadyInShipment ? (
<p className="text-xs text-text-muted">{t('labDispatchEmpty')}</p>
) : activeLabCase ? (
<div className="space-y-4 border border-border/60 rounded-[var(--radius-md)] p-4 bg-background-secondary/30">
<div className="space-y-4 border border-border/60 rounded-[var(--radius-md)] p-3 sm:p-4 bg-background-secondary/30">
{renderShipmentCardHeader()}
{sent ? (
<>
@@ -513,6 +513,7 @@ export function LabCasesDispatchPanel({
<Button
type="button"
variant="primary"
className="w-full sm:w-auto"
disabled={
disabled ||
sendBusyId === activeLabCase.clientId ||