fix(timesheet): refine responsive filter bar and timer actions
This commit is contained in:
@@ -242,7 +242,7 @@ export default function TimesheetFilterBar({
|
|||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-slate-200 bg-white p-2.5 shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
<div className="rounded-lg border border-slate-200 bg-white p-2.5 shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
<div className="flex items-center gap-2">
|
||||||
<div className="relative min-w-0 flex-1">
|
<div className="relative min-w-0 flex-1">
|
||||||
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400 rtl:left-auto rtl:right-3" />
|
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400 rtl:left-auto rtl:right-3" />
|
||||||
<input
|
<input
|
||||||
@@ -254,24 +254,25 @@ export default function TimesheetFilterBar({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex shrink-0 items-center gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsExpanded((current) => !current)}
|
onClick={() => setIsExpanded((current) => !current)}
|
||||||
className={`inline-flex h-9 items-center gap-2 rounded-md border px-3 text-sm transition-colors ${
|
aria-label={isExpanded ? (labels?.hideFilters || "Hide filters") : (labels?.showFilters || "Filters")}
|
||||||
|
className={`relative inline-flex h-9 w-9 items-center justify-center rounded-md border text-sm transition-colors sm:w-auto sm:gap-2 sm:px-3 ${
|
||||||
isExpanded || hasActiveFilters
|
isExpanded || hasActiveFilters
|
||||||
? "border-sky-200 bg-sky-50 text-sky-700 dark:border-sky-500/30 dark:bg-sky-500/15 dark:text-sky-300"
|
? "border-sky-200 bg-sky-50 text-sky-700 dark:border-sky-500/30 dark:bg-sky-500/15 dark:text-sky-300"
|
||||||
: "border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:text-slate-900 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200 dark:hover:border-slate-600 dark:hover:text-white"
|
: "border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:text-slate-900 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200 dark:hover:border-slate-600 dark:hover:text-white"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<SlidersHorizontal className="h-4 w-4" />
|
<SlidersHorizontal className="h-4 w-4" />
|
||||||
<span>{isExpanded ? (labels?.hideFilters || "Hide filters") : (labels?.showFilters || "Filters")}</span>
|
<span className="hidden sm:inline">{isExpanded ? (labels?.hideFilters || "Hide filters") : (labels?.showFilters || "Filters")}</span>
|
||||||
{hasActiveFilters && (
|
{hasActiveFilters && (
|
||||||
<span className="inline-flex min-w-5 items-center justify-center rounded-full bg-sky-600 px-1.5 text-[11px] font-semibold text-white dark:bg-sky-500">
|
<span className="absolute -right-1 -top-1 z-10 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-sky-600 px-1 text-[10px] font-semibold leading-none text-white dark:bg-sky-500 sm:static sm:z-auto sm:h-auto sm:min-w-5 sm:px-1.5 sm:text-[11px] sm:leading-normal">
|
||||||
{activeChips.length}
|
{activeChips.length}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<ChevronDown className={`h-4 w-4 transition-transform ${isExpanded ? "rotate-180" : ""}`} />
|
<ChevronDown className={`hidden h-4 w-4 transition-transform sm:inline ${isExpanded ? "rotate-180" : ""}`} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -288,97 +289,94 @@ export default function TimesheetFilterBar({
|
|||||||
onClearFilters();
|
onClearFilters();
|
||||||
}}
|
}}
|
||||||
disabled={!hasActiveFilters}
|
disabled={!hasActiveFilters}
|
||||||
className="inline-flex h-9 items-center gap-2 rounded-md border border-slate-200 bg-white px-3 text-sm text-slate-600 transition hover:border-slate-300 hover:text-slate-900 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:border-slate-600 dark:hover:text-white"
|
aria-label={labels?.clear || "Clear"}
|
||||||
|
className={`inline-flex h-9 w-9 items-center justify-center rounded-md border text-sm transition sm:w-auto sm:gap-2 sm:px-3 ${
|
||||||
|
hasActiveFilters
|
||||||
|
? "border-red-200 bg-red-50 text-red-700 hover:border-red-300 hover:bg-red-100 hover:text-red-800 dark:border-red-500/30 dark:bg-red-500/15 dark:text-red-300 dark:hover:border-red-400 dark:hover:bg-red-500/20 dark:hover:text-red-200"
|
||||||
|
: "border-slate-200 bg-white text-slate-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
{labels?.clear || "Clear"}
|
<span className="hidden sm:inline">{labels?.clear || "Clear"}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => onApply(draftSearchQuery, draftFilters)}
|
|
||||||
className="inline-flex h-9 items-center gap-2 rounded-md border border-sky-600 bg-sky-600 px-3 text-sm font-medium text-white transition hover:border-sky-700 hover:bg-sky-700 dark:border-sky-500 dark:bg-sky-500 dark:hover:border-sky-400 dark:hover:bg-sky-400"
|
|
||||||
>
|
|
||||||
{labels?.apply || "Apply"}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeChips.length > 0 && (
|
|
||||||
<div className="flex flex-wrap gap-1.5">
|
|
||||||
{activeChips.map((chip) => (
|
|
||||||
<span
|
|
||||||
key={chip}
|
|
||||||
className="inline-flex items-center rounded-full bg-slate-100 px-2.5 py-1 text-[11px] font-medium text-slate-600 dark:bg-slate-800 dark:text-slate-200"
|
|
||||||
>
|
|
||||||
{chip}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className="grid gap-2 border-t border-slate-200 pt-2 dark:border-slate-800 xl:grid-cols-[minmax(0,0.9fr)_minmax(0,0.9fr)_minmax(0,1fr)_minmax(0,1fr)_minmax(0,1.2fr)]">
|
<div className="border-t border-slate-200 pt-2 dark:border-slate-800">
|
||||||
<MiniFilterBlock icon={<CalendarRange className="h-3.5 w-3.5" />} label={labels?.customFrom || "From"}>
|
<div className="grid gap-2 xl:grid-cols-[minmax(0,0.9fr)_minmax(0,0.9fr)_minmax(0,1fr)_minmax(0,1fr)_minmax(0,1.2fr)]">
|
||||||
<JalaliDatePicker
|
<MiniFilterBlock icon={<CalendarRange className="h-3.5 w-3.5" />} label={labels?.customFrom || "From"}>
|
||||||
value={draftFilters.startedAfter}
|
<JalaliDatePicker
|
||||||
onChange={(value) => setDraftFilters((current) => ({ ...current, startedAfter: value }))}
|
value={draftFilters.startedAfter}
|
||||||
placeholder="YYYY/MM/DD"
|
onChange={(value) => setDraftFilters((current) => ({ ...current, startedAfter: value }))}
|
||||||
inputClassName="h-8 border border-slate-200 bg-white px-2 text-sm shadow-none focus:ring-0 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
|
placeholder="YYYY/MM/DD"
|
||||||
/>
|
inputClassName="h-8 border border-slate-200 bg-white px-2 text-sm shadow-none focus:ring-0 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
|
||||||
</MiniFilterBlock>
|
/>
|
||||||
|
</MiniFilterBlock>
|
||||||
|
|
||||||
<MiniFilterBlock icon={<CalendarRange className="h-3.5 w-3.5" />} label={labels?.customTo || "To"}>
|
<MiniFilterBlock icon={<CalendarRange className="h-3.5 w-3.5" />} label={labels?.customTo || "To"}>
|
||||||
<JalaliDatePicker
|
<JalaliDatePicker
|
||||||
value={draftFilters.startedBefore}
|
value={draftFilters.startedBefore}
|
||||||
onChange={(value) => setDraftFilters((current) => ({ ...current, startedBefore: value }))}
|
onChange={(value) => setDraftFilters((current) => ({ ...current, startedBefore: value }))}
|
||||||
placeholder="YYYY/MM/DD"
|
placeholder="YYYY/MM/DD"
|
||||||
inputClassName="h-8 border border-slate-200 bg-white px-2 text-sm shadow-none focus:ring-0 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
|
inputClassName="h-8 border border-slate-200 bg-white px-2 text-sm shadow-none focus:ring-0 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
|
||||||
/>
|
/>
|
||||||
</MiniFilterBlock>
|
</MiniFilterBlock>
|
||||||
|
|
||||||
<MiniFilterBlock icon={<BriefcaseBusiness className="h-3.5 w-3.5" />} label={labels?.client || "Client"}>
|
<MiniFilterBlock icon={<BriefcaseBusiness className="h-3.5 w-3.5" />} label={labels?.client || "Client"}>
|
||||||
<Select
|
<Select
|
||||||
value={draftFilters.clientId}
|
value={draftFilters.clientId}
|
||||||
onChange={(clientId) =>
|
onChange={(clientId) =>
|
||||||
setDraftFilters((current) => ({
|
setDraftFilters((current) => ({
|
||||||
...current,
|
...current,
|
||||||
clientId,
|
clientId,
|
||||||
projectId:
|
projectId:
|
||||||
current.projectId &&
|
current.projectId &&
|
||||||
!projects.some((project) => project.id === current.projectId && project.client?.id === clientId)
|
!projects.some((project) => project.id === current.projectId && project.client?.id === clientId)
|
||||||
? ""
|
? ""
|
||||||
: current.projectId,
|
: current.projectId,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
options={[{ value: "", label: labels?.allClients || "All clients" }, ...clients]}
|
options={[{ value: "", label: labels?.allClients || "All clients" }, ...clients]}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
buttonClassName="h-8 w-full rounded-md border border-slate-200 bg-white px-2 text-sm shadow-none focus:ring-0 dark:border-slate-700 dark:bg-slate-900"
|
buttonClassName="h-8 w-full rounded-md border border-slate-200 bg-white px-2 text-sm shadow-none focus:ring-0 dark:border-slate-700 dark:bg-slate-900"
|
||||||
/>
|
/>
|
||||||
</MiniFilterBlock>
|
</MiniFilterBlock>
|
||||||
|
|
||||||
<MiniFilterBlock icon={<FolderKanban className="h-3.5 w-3.5" />} label={labels?.project || "Project"}>
|
<MiniFilterBlock icon={<FolderKanban className="h-3.5 w-3.5" />} label={labels?.project || "Project"}>
|
||||||
<Select
|
<Select
|
||||||
value={draftFilters.projectId}
|
value={draftFilters.projectId}
|
||||||
onChange={(projectId) => setDraftFilters((current) => ({ ...current, projectId }))}
|
onChange={(projectId) => setDraftFilters((current) => ({ ...current, projectId }))}
|
||||||
options={[{ value: "", label: labels?.allProjects || "All projects" }, ...(
|
options={[{ value: "", label: labels?.allProjects || "All projects" }, ...(
|
||||||
draftFilters.clientId
|
draftFilters.clientId
|
||||||
? projects.filter((project) => project.client?.id === draftFilters.clientId)
|
? projects.filter((project) => project.client?.id === draftFilters.clientId)
|
||||||
: projects
|
: projects
|
||||||
).map((project) => ({ value: project.id, label: project.name }))]}
|
).map((project) => ({ value: project.id, label: project.name }))]}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
buttonClassName="h-8 w-full rounded-md border border-slate-200 bg-white px-2 text-sm shadow-none focus:ring-0 dark:border-slate-700 dark:bg-slate-900"
|
buttonClassName="h-8 w-full rounded-md border border-slate-200 bg-white px-2 text-sm shadow-none focus:ring-0 dark:border-slate-700 dark:bg-slate-900"
|
||||||
/>
|
/>
|
||||||
</MiniFilterBlock>
|
</MiniFilterBlock>
|
||||||
|
|
||||||
<MiniFilterBlock icon={<TagIcon className="h-3.5 w-3.5" />} label={labels?.tags || "Tags"}>
|
<MiniFilterBlock icon={<TagIcon className="h-3.5 w-3.5" />} label={labels?.tags || "Tags"}>
|
||||||
<FilterTagMultiSelect
|
<FilterTagMultiSelect
|
||||||
tags={tags}
|
tags={tags}
|
||||||
selectedTagIds={draftFilters.tagIds}
|
selectedTagIds={draftFilters.tagIds}
|
||||||
onChange={(tagIds) => setDraftFilters((current) => ({ ...current, tagIds }))}
|
onChange={(tagIds) => setDraftFilters((current) => ({ ...current, tagIds }))}
|
||||||
title={labels?.allTags || "All tags"}
|
title={labels?.allTags || "All tags"}
|
||||||
/>
|
/>
|
||||||
</MiniFilterBlock>
|
</MiniFilterBlock>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2 flex justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onApply(draftSearchQuery, draftFilters)}
|
||||||
|
className="inline-flex h-9 w-full items-center justify-center gap-2 rounded-md border bg-sky-50 border-sky-200 text-sky-700 dark:border-sky-500/30 dark:bg-sky-500/15 dark:text-sky-300 px-3 text-sm font-medium transition hover:border-sky-700 hover:bg-sky-700 hover:text-sky-100 dark:hover:border-sky-400 dark:hover:text-sky-900 dark:hover:bg-sky-400"
|
||||||
|
>
|
||||||
|
{labels?.apply || "Apply"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2097,11 +2097,6 @@ export default function Timesheet() {
|
|||||||
<h1 className="text-sm font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400">
|
<h1 className="text-sm font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400">
|
||||||
{t.timesheet?.title || "Timesheet"}
|
{t.timesheet?.title || "Timesheet"}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<Button onClick={openCreateModal} className="h-9 rounded-md px-3 text-xs font-semibold uppercase dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-700">
|
|
||||||
<Plus className="me-2 h-4 w-4" />
|
|
||||||
{t.timesheet?.addEntry || "Add Entry"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-4 hidden overflow-x-auto border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-950 md:block">
|
<div className="mb-4 hidden overflow-x-auto border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-950 md:block">
|
||||||
|
|||||||
Reference in New Issue
Block a user