feat: Mandanten-Zuordnung sichtbar auf Workflow-Karten + Mandanten-Filter
- Mandantenname auf allen Workflow-Karten (Grid + Liste) - Mandantenname im Dashboard bei aktuellen Workflows - Mandanten-Filter-Dropdown in der Workflow-Liste (für Admins/Techniker) - Tenant-Info im Workflow-Typ ergänzt
This commit is contained in:
parent
7f9e302e5e
commit
b0561ce714
@ -217,6 +217,10 @@ export default function Dashboard() {
|
|||||||
{wf.title}
|
{wf.title}
|
||||||
</p>
|
</p>
|
||||||
<p className="truncate text-sm text-gray-500">
|
<p className="truncate text-sm text-gray-500">
|
||||||
|
{wf.tenant && (
|
||||||
|
<span className="font-medium text-primary-600">{wf.tenant.name}</span>
|
||||||
|
)}
|
||||||
|
{wf.tenant && <span> · </span>}
|
||||||
{wf._count?.steps ?? wf.steps?.length ?? 0} Schritte · v{wf.version}
|
{wf._count?.steps ?? wf.steps?.length ?? 0} Schritte · v{wf.version}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Link, useSearchParams } from 'react-router-dom';
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
|
Building2,
|
||||||
FileText,
|
FileText,
|
||||||
Grid3X3,
|
Grid3X3,
|
||||||
List,
|
List,
|
||||||
@ -8,8 +9,9 @@ import {
|
|||||||
Tag,
|
Tag,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { workflowsApi, categoriesApi } from '../services/api';
|
import { workflowsApi, categoriesApi, tenantsApi } from '../services/api';
|
||||||
import type { Category, Workflow } from '../types';
|
import { useAuth } from '../hooks/useAuth';
|
||||||
|
import type { Category, Tenant, Workflow } from '../types';
|
||||||
import SearchBar from '../components/SearchBar';
|
import SearchBar from '../components/SearchBar';
|
||||||
import StatusBadge from '../components/StatusBadge';
|
import StatusBadge from '../components/StatusBadge';
|
||||||
import Pagination from '../components/Pagination';
|
import Pagination from '../components/Pagination';
|
||||||
@ -28,12 +30,16 @@ const statusOptions: { value: string; label: string }[] = [
|
|||||||
export default function WorkflowList() {
|
export default function WorkflowList() {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const isTemplate = searchParams.get('template') === 'true';
|
const isTemplate = searchParams.get('template') === 'true';
|
||||||
|
const { user } = useAuth();
|
||||||
|
const isAdmin = user?.role === 'SYSTEM_ADMIN' || user?.role === 'TECHNICIAN';
|
||||||
|
|
||||||
const [workflows, setWorkflows] = useState<Workflow[]>([]);
|
const [workflows, setWorkflows] = useState<Workflow[]>([]);
|
||||||
const [categories, setCategories] = useState<Category[]>([]);
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
|
const [tenants, setTenants] = useState<Tenant[]>([]);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [statusFilter, setStatusFilter] = useState('');
|
const [statusFilter, setStatusFilter] = useState('');
|
||||||
const [categoryFilter, setCategoryFilter] = useState('');
|
const [categoryFilter, setCategoryFilter] = useState('');
|
||||||
|
const [tenantFilter, setTenantFilter] = useState('');
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [totalPages, setTotalPages] = useState(1);
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@ -41,11 +47,14 @@ export default function WorkflowList() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
categoriesApi.list().then(setCategories).catch(() => {});
|
categoriesApi.list().then(setCategories).catch(() => {});
|
||||||
}, []);
|
if (isAdmin) {
|
||||||
|
tenantsApi.list({ pageSize: 100 }).then((r) => setTenants(r.data)).catch(() => {});
|
||||||
|
}
|
||||||
|
}, [isAdmin]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadWorkflows();
|
loadWorkflows();
|
||||||
}, [search, statusFilter, categoryFilter, page, isTemplate]);
|
}, [search, statusFilter, categoryFilter, tenantFilter, page, isTemplate]);
|
||||||
|
|
||||||
const loadWorkflows = async () => {
|
const loadWorkflows = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -57,6 +66,7 @@ export default function WorkflowList() {
|
|||||||
status: statusFilter || undefined,
|
status: statusFilter || undefined,
|
||||||
categoryId: categoryFilter || undefined,
|
categoryId: categoryFilter || undefined,
|
||||||
isTemplate: isTemplate || undefined,
|
isTemplate: isTemplate || undefined,
|
||||||
|
tenantId: tenantFilter || undefined,
|
||||||
};
|
};
|
||||||
const res = await workflowsApi.list(params);
|
const res = await workflowsApi.list(params);
|
||||||
setWorkflows(res.data);
|
setWorkflows(res.data);
|
||||||
@ -130,6 +140,23 @@ export default function WorkflowList() {
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
{isAdmin && tenants.length > 0 && (
|
||||||
|
<select
|
||||||
|
value={tenantFilter}
|
||||||
|
onChange={(e) => {
|
||||||
|
setTenantFilter(e.target.value);
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
className="input-field w-auto"
|
||||||
|
>
|
||||||
|
<option value="">Alle Mandanten</option>
|
||||||
|
{tenants.map((t) => (
|
||||||
|
<option key={t.id} value={t.id}>
|
||||||
|
{t.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
<div className="flex rounded-lg border border-gray-300 bg-white">
|
<div className="flex rounded-lg border border-gray-300 bg-white">
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewMode('grid')}
|
onClick={() => setViewMode('grid')}
|
||||||
@ -190,8 +217,14 @@ export default function WorkflowList() {
|
|||||||
</h3>
|
</h3>
|
||||||
<StatusBadge status={wf.status} />
|
<StatusBadge status={wf.status} />
|
||||||
</div>
|
</div>
|
||||||
|
{wf.tenant && (
|
||||||
|
<div className="mt-2 flex items-center gap-1.5 text-xs font-medium text-primary-600">
|
||||||
|
<Building2 size={12} />
|
||||||
|
{wf.tenant.name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{wf.description && (
|
{wf.description && (
|
||||||
<p className="mt-2 line-clamp-2 text-sm text-gray-500">
|
<p className="mt-1 line-clamp-2 text-sm text-gray-500">
|
||||||
{wf.description}
|
{wf.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@ -242,11 +275,13 @@ export default function WorkflowList() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="truncate font-medium text-gray-900">{wf.title}</p>
|
<p className="truncate font-medium text-gray-900">{wf.title}</p>
|
||||||
{wf.description && (
|
<p className="truncate text-sm text-gray-500">
|
||||||
<p className="truncate text-sm text-gray-500">
|
{wf.tenant && (
|
||||||
{wf.description}
|
<span className="font-medium text-primary-600">{wf.tenant.name}</span>
|
||||||
</p>
|
)}
|
||||||
)}
|
{wf.tenant && wf.description && <span> · </span>}
|
||||||
|
{wf.description}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{wf.category && (
|
{wf.category && (
|
||||||
<span className="hidden rounded bg-gray-100 px-2 py-1 text-xs text-gray-600 md:inline">
|
<span className="hidden rounded bg-gray-100 px-2 py-1 text-xs text-gray-600 md:inline">
|
||||||
|
|||||||
@ -29,6 +29,7 @@ export interface Workflow {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
steps?: WorkflowStep[];
|
steps?: WorkflowStep[];
|
||||||
_count?: { steps: number };
|
_count?: { steps: number };
|
||||||
|
tenant?: Tenant;
|
||||||
createdBy: User;
|
createdBy: User;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user