feat(workspaces): add current user rates panel
This commit is contained in:
196
src/components/rates/WorkspaceRatesPanel.tsx
Normal file
196
src/components/rates/WorkspaceRatesPanel.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Banknote, BriefcaseBusiness, FolderKanban, X } from "lucide-react";
|
||||
|
||||
import type { MyWorkspaceRatesResponse } from "../../api/rates";
|
||||
import { useTranslation } from "../../hooks/useTranslation";
|
||||
import { formatRateDisplay } from "../../lib/money";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
export function WorkspaceRatesPanel({
|
||||
open,
|
||||
data,
|
||||
isLoading,
|
||||
onClose,
|
||||
}: {
|
||||
open: boolean;
|
||||
data: MyWorkspaceRatesResponse | null;
|
||||
isLoading: boolean;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { t, lang } = useTranslation();
|
||||
const [shouldRender, setShouldRender] = useState(open);
|
||||
const [isVisible, setIsVisible] = useState(open);
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
let frameId: number | null = null;
|
||||
|
||||
if (open) {
|
||||
setShouldRender(true);
|
||||
frameId = window.requestAnimationFrame(() => setIsVisible(true));
|
||||
} else {
|
||||
setIsVisible(false);
|
||||
timeoutId = setTimeout(() => setShouldRender(false), 300);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
if (frameId) window.cancelAnimationFrame(frameId);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
if (!shouldRender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[80] flex items-end lg:items-stretch lg:justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className={`absolute inset-0 cursor-pointer bg-slate-950/40 backdrop-blur-[2px] transition-opacity duration-300 ${
|
||||
isVisible ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
onClick={onClose}
|
||||
aria-label="Close rates panel"
|
||||
/>
|
||||
<aside
|
||||
className={`relative z-10 flex max-h-[88vh] w-full flex-col rounded-t-[2rem] bg-white shadow-2xl transition-transform duration-300 ease-out dark:bg-slate-950 lg:h-full lg:max-h-none lg:w-[34rem] lg:rounded-none lg:border-l lg:border-slate-800 ${
|
||||
isVisible ? "translate-y-0 lg:translate-x-0" : "translate-y-full lg:translate-x-full"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-slate-200 px-5 py-4 dark:border-slate-800">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-slate-900 dark:text-white">
|
||||
{t.rates?.myRatesTitle || "My rates"}
|
||||
</h2>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||
{t.rates?.myRatesHint || "Project-specific rates override your workspace rate in this workspace."}
|
||||
</p>
|
||||
</div>
|
||||
<Button type="button" variant="ghost" size="icon" onClick={onClose} className="rounded-xl">
|
||||
<X className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-5 py-5">
|
||||
{isLoading ? (
|
||||
<div className="rounded-2xl border border-slate-200 bg-slate-50 p-4 text-sm text-slate-500 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-400">
|
||||
{t.loading || "Loading..."}
|
||||
</div>
|
||||
) : !data ? (
|
||||
<div className="rounded-2xl border border-dashed border-slate-300 p-6 text-center text-sm text-slate-500 dark:border-slate-700 dark:text-slate-400">
|
||||
{t.rates?.myRatesEmpty || "No rates are available for this workspace yet."}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-5">
|
||||
<div className="rounded-3xl border border-slate-200 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-900">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-emerald-100 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-300">
|
||||
<Banknote className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-base font-semibold text-slate-900 dark:text-white">
|
||||
{t.rates?.workspaceRate || "Workspace rate"}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
|
||||
{t.rates?.workspaceRateHint || "This is your default rate unless a project-specific rate overrides it."}
|
||||
</p>
|
||||
<div className="mt-3 text-lg font-bold text-slate-900 dark:text-white">
|
||||
{data.workspace_rate
|
||||
? formatRateDisplay(
|
||||
{
|
||||
hourly_rate: data.workspace_rate.hourly_rate,
|
||||
currency: data.workspace_rate.currency,
|
||||
price_unit: data.workspace_rate.price_unit,
|
||||
},
|
||||
lang,
|
||||
)
|
||||
: (t.rates?.noRate || "No rate")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<div className="rounded-2xl border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900">
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-slate-500 dark:text-slate-400">
|
||||
{t.rates?.accessibleProjects || "Accessible projects"}
|
||||
</div>
|
||||
<div className="mt-2 text-2xl font-bold text-slate-900 dark:text-white">
|
||||
{data.accessible_project_count}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900">
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-slate-500 dark:text-slate-400">
|
||||
{t.rates?.projectOverrides || "Project overrides"}
|
||||
</div>
|
||||
<div className="mt-2 text-2xl font-bold text-slate-900 dark:text-white">
|
||||
{data.project_override_count}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-900">
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-slate-500 dark:text-slate-400">
|
||||
{t.rates?.workspaceFallbackProjects || "Using workspace rate"}
|
||||
</div>
|
||||
<div className="mt-2 text-2xl font-bold text-slate-900 dark:text-white">
|
||||
{data.workspace_fallback_project_count}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-3xl border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900">
|
||||
<div className="border-b border-slate-100 px-5 py-4 dark:border-slate-800">
|
||||
<h3 className="text-base font-semibold text-slate-900 dark:text-white">
|
||||
{t.rates?.projectSectionTitle || "Project user rates"}
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
|
||||
{t.rates?.projectOverrideHint || "Only projects with custom overrides are listed here. Other accessible projects use your workspace rate."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{data.project_rates.length ? (
|
||||
<div className="divide-y divide-slate-100 dark:divide-slate-800">
|
||||
{data.project_rates.map((projectRate) => (
|
||||
<div key={projectRate.project.id} className="flex items-start gap-4 px-5 py-4">
|
||||
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl bg-sky-100 text-sky-700 dark:bg-sky-500/10 dark:text-sky-300">
|
||||
<FolderKanban className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<p className="text-sm font-semibold text-slate-900 dark:text-white">
|
||||
{projectRate.project.name}
|
||||
</p>
|
||||
{projectRate.project.client ? (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-slate-100 px-2 py-1 text-[11px] font-medium text-slate-600 dark:bg-slate-800 dark:text-slate-300">
|
||||
<BriefcaseBusiness className="h-3 w-3" />
|
||||
{projectRate.project.client.name}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="mt-2 text-sm font-semibold text-slate-900 dark:text-white">
|
||||
{formatRateDisplay(
|
||||
{
|
||||
hourly_rate: projectRate.rate.hourly_rate,
|
||||
currency: projectRate.rate.currency,
|
||||
price_unit: projectRate.rate.price_unit,
|
||||
},
|
||||
lang,
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-5 py-6 text-sm text-slate-500 dark:text-slate-400">
|
||||
{t.rates?.projectOverrideEmpty || "You do not have any project-specific rate overrides in this workspace."}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user