Files
e-trznice/frontend/src/pages/manager/Users.jsx
2025-10-02 00:54:34 +02:00

796 lines
28 KiB
JavaScript

import React, { useEffect, useMemo, useState } from "react";
import Table from "../../components/Table";
import Sidebar from "../../components/Sidebar";
import {
Container,
Row,
Col,
Button as BootstrapButton,
Modal,
Form,
Alert,
} from "react-bootstrap";
import {
ActionIcon,
Group,
TextInput,
Text,
Stack,
Button,
Switch,
Badge,
Tooltip,
} from "@mantine/core";
import { IconSearch, IconX, IconEye, IconEdit, IconTrash, IconPlus, IconReceipt2 } from "@tabler/icons-react";
import userAPI from "../../api/model/user";
import { fetchEnumFromSchemaJson } from "../../api/get_chocies";
function Users() {
// State
const [users, setUsers] = useState([]);
const [fetching, setFetching] = useState(true);
// Separate filter states for each field
const [filterUsername, setFilterUsername] = useState("");
const [filterEmail, setFilterEmail] = useState("");
const [filterFirstName, setFilterFirstName] = useState("");
const [filterLastName, setFilterLastName] = useState("");
const [selectedRoles, setSelectedRoles] = useState([]);
const [selectedAccountTypes, setSelectedAccountTypes] = useState([]);
const [selectedActive, setSelectedActive] = useState([]);
const [selectedEmailVerified, setSelectedEmailVerified] = useState([]); // new filter state
const [filterCity, setFilterCity] = useState("");
const [filterPSC, setFilterPSC] = useState("");
const [showModal, setShowModal] = useState(false);
const [modalType, setModalType] = useState('view'); // 'view', 'edit'
const [selectedUser, setSelectedUser] = useState(null);
// Add more fields to formData for editing
const [formData, setFormData] = useState({
username: "",
email: "",
first_name: "",
last_name: "",
role: "",
account_type: "",
email_verified: false,
phone_number: "",
city: "",
street: "",
PSC: "",
bank_account: "",
ICO: "",
RC: "",
GDPR: false,
is_active: true,
var_symbol: "", // <-- add this line
});
const [error, setError] = useState(null);
const [submitting, setSubmitting] = useState(false);
const [roleDropdownOptions, setRoleDropdownOptions] = useState([]);
const [accountTypeDropdownOptions, setAccountTypeDropdownOptions] = useState([]);
// Fetch users
useEffect(() => {
const fetchData = async () => {
try {
console.log("Fetching users...");
const data = await userAPI.getUsers();
// Defensive: check if response is array, otherwise log error and set empty array
if (Array.isArray(data)) {
console.log("Fetched users:", data);
setUsers(data);
} else if (data && Array.isArray(data.results)) {
// DRF pagination: { count, next, previous, results }
console.log("Fetched users (paginated):", data.results);
setUsers(data.results);
} else {
console.error("Fetched users is not an array:", data);
setUsers([]);
}
} catch (err) {
console.error("Error fetching users:", err);
setUsers([]);
} finally {
setFetching(false);
}
};
fetchData();
}, []);
useEffect(() => {
// Načti možnosti pro role
fetchEnumFromSchemaJson("/api/account/users/", "post", "role")
.then((choices) => setRoleDropdownOptions(choices))
.catch(() => setRoleDropdownOptions([
{ value: "admin", label: "Administrátor" },
{ value: "seller", label: "Prodejce" },
{ value: "squareManager", label: "Správce tržiště" },
{ value: "cityClerk", label: "Úředník" },
{ value: "checker", label: "Kontrolor" },
]));
// Načti možnosti pro typ účtu
fetchEnumFromSchemaJson("/api/account/users/", "post", "account_type")
.then((choices) => setAccountTypeDropdownOptions(choices))
.catch(() => setAccountTypeDropdownOptions([
{ value: "company", label: "Firma" },
{ value: "individual", label: "Fyzická osoba" },
]));
}, []);
// Role/group options
const roleOptions = useMemo(() => {
if (!Array.isArray(users)) return [];
const allRoles = users.map(u => u.role).filter(Boolean);
return [...new Set(allRoles)];
}, [users]);
const accountTypeOptions = useMemo(() => {
if (!Array.isArray(users)) return [];
const allTypes = users.map(u => u.account_type).filter(Boolean);
return [...new Set(allTypes)];
}, [users]);
// Filtering
const filteredUsers = useMemo(() => {
let data = Array.isArray(users) ? users : [];
if (filterUsername) {
const q = filterUsername.toLowerCase();
data = data.filter(u => u.username?.toLowerCase().includes(q));
}
if (filterEmail) {
const q = filterEmail.toLowerCase();
data = data.filter(u => u.email?.toLowerCase().includes(q));
}
if (filterFirstName) {
const q = filterFirstName.toLowerCase();
data = data.filter(u => u.first_name?.toLowerCase().includes(q));
}
if (filterLastName) {
const q = filterLastName.toLowerCase();
data = data.filter(u => u.last_name?.toLowerCase().includes(q));
}
if (selectedRoles.length > 0) {
data = data.filter(u => selectedRoles.includes(u.role));
}
if (selectedAccountTypes.length > 0) {
data = data.filter(u => selectedAccountTypes.includes(u.account_type));
}
if (selectedActive.length > 0) {
data = data.filter(u =>
selectedActive.includes(u.is_active ? "true" : "false")
);
}
if (selectedEmailVerified.length > 0) {
data = data.filter(u =>
selectedEmailVerified.includes(u.email_verified ? "true" : "false")
);
}
if (filterCity) {
const q = filterCity.toLowerCase();
data = data.filter(u => u.city?.toLowerCase().includes(q));
}
if (filterPSC) {
const q = filterPSC.toLowerCase();
data = data.filter(u => u.PSC?.toLowerCase().includes(q));
}
return data;
}, [users, filterUsername, filterEmail, filterFirstName, filterLastName, selectedRoles, selectedAccountTypes, selectedActive, selectedEmailVerified, filterCity, filterPSC]);
// Handlers
const handleShowUser = (user) => {
console.log("Show user:", user);
setSelectedUser(user);
setModalType('view');
setShowModal(true);
};
const handleEditUser = (user) => {
console.log("Edit user:", user);
setSelectedUser(user);
setFormData({
username: user.username || "",
email: user.email || "",
first_name: user.first_name || "",
last_name: user.last_name || "",
role: user.role || "",
account_type: user.account_type || "",
email_verified: user.email_verified || false,
phone_number: user.phone_number || "",
city: user.city || "",
street: user.street || "",
PSC: user.PSC || "",
bank_account: user.bank_account || "",
ICO: user.ICO || "",
RC: user.RC || "",
GDPR: user.GDPR || false,
is_active: user.is_active ?? true,
var_symbol: user.var_symbol || "",
});
setModalType('edit');
setShowModal(true);
setError(null);
};
const handleDeleteUser = async (user) => {
console.log("Delete user:", user);
if (window.confirm(`Opravdu smazat uživatele: ${user.username}?`)) {
await userAPI.deleteUser(user.id);
const data = await userAPI.getUsers();
setUsers(data);
}
};
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
console.log("Form change:", name, type === "checkbox" ? checked : value);
setFormData((old) => ({
...old,
[name]: type === "checkbox" ? checked : value,
}));
};
const handleGroupsChange = (groups) => {
console.log("Groups changed:", groups);
setFormData((old) => ({ ...old, groups }));
};
const handleEditModalSubmit = async (e) => {
e.preventDefault();
setError(null);
setSubmitting(true);
console.log("Submitting edit:", formData);
try {
await userAPI.updateUser(selectedUser.id, {
...formData,
account_type: formData.account_type,
var_symbol: formData.var_symbol, // <-- ensure this is sent
});
setShowModal(false);
const data = await userAPI.getUsers();
setUsers(data);
} catch (err) {
const apiErrors = err.response?.data;
if (typeof apiErrors === "object") {
const messages = Object.entries(apiErrors)
.map(([key, value]) => `${key}: ${Array.isArray(value) ? value.join(", ") : value}`)
.join("\n");
setError("Chyba při ukládání:\n" + messages);
console.log("API error:", apiErrors);
} else {
setError("Chyba při ukládání: " + (err.message || "Neznámá chyba"));
console.log("Unknown error:", err);
}
} finally {
setSubmitting(false);
}
};
// Table columns
const columns = [
{ accessor: "id",
title: "#",
sortable: true,
width: "2%" },
{
accessor: "username",
title: "Uživatelské jméno",
sortable: true,
width: "7%",
filter: (
<TextInput
label="Filtrovat username"
placeholder="Zadej uživatelské jméno"
leftSection={<IconSearch size={16} />}
rightSection={
<ActionIcon size="sm" variant="transparent" c="dimmed" onClick={() => setFilterUsername("")}>
<IconX size={14} />
</ActionIcon>
}
value={filterUsername}
onChange={(e) => setFilterUsername(e.currentTarget.value)}
/>
),
filtering: !!filterUsername,
},
{
accessor: "email",
title: "Email",
sortable: true,
width: "7%",
filter: (
<TextInput
label="Filtrovat email"
placeholder="Zadej email"
leftSection={<IconSearch size={16} />}
rightSection={
<ActionIcon size="sm" variant="transparent" c="dimmed" onClick={() => setFilterEmail("")}>
<IconX size={14} />
</ActionIcon>
}
value={filterEmail}
onChange={(e) => setFilterEmail(e.currentTarget.value)}
/>
),
filtering: !!filterEmail,
},
{
accessor: "first_name",
title: "Jméno",
sortable: true,
width: "5%",
filter: (
<TextInput
label="Filtrovat jméno"
placeholder="Zadej jméno"
leftSection={<IconSearch size={16} />}
rightSection={
<ActionIcon size="sm" variant="transparent" c="dimmed" onClick={() => setFilterFirstName("")}>
<IconX size={14} />
</ActionIcon>
}
value={filterFirstName}
onChange={(e) => setFilterFirstName(e.currentTarget.value)}
/>
),
filtering: !!filterFirstName,
},
{
accessor: "last_name",
title: "Příjmení",
sortable: true,
width: "5%",
filter: (
<TextInput
label="Filtrovat příjmení"
placeholder="Zadej příjmení"
leftSection={<IconSearch size={16} />}
rightSection={
<ActionIcon size="sm" variant="transparent" c="dimmed" onClick={() => setFilterLastName("")}>
<IconX size={14} />
</ActionIcon>
}
value={filterLastName}
onChange={(e) => setFilterLastName(e.currentTarget.value)}
/>
),
filtering: !!filterLastName,
},
{
accessor: "role",
title: "Role",
width: "7%",
render: (row) =>
row.role ? (
<Badge color="blue" variant="light">
{
{
"admin": "Administrátor",
"seller": "Prodejce",
"squareManager": "Správce tržiště",
"cityClerk": "Úředník",
"checker": "Kontrolor",
}[row.role] || row.role
}
</Badge>
) : (
<Text c="dimmed" fs="italic">Žádná</Text>
),
filter: (() => {
const toggle = (val) => {
setSelectedRoles(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val]);
};
const roleBadges = roleOptions.map(role => {
const label = {
"admin": "Administrátor",
"seller": "Prodejce",
"squareManager": "Správce tržiště",
"cityClerk": "Úředník",
"checker": "Kontrolor",
}[role] || role;
const active = selectedRoles.includes(role);
return (
<Badge
key={role}
color={active ? 'blue' : 'gray'}
variant={active ? 'filled' : 'outline'}
style={{ cursor: 'pointer', userSelect: 'none' }}
onClick={() => toggle(role)}
aria-pressed={active}
role="button"
>
{label}
</Badge>
);
});
return (
<Stack gap={4} style={{ minWidth: 170 }}>
<Text fw={500} size="xs" c="dimmed">Filtrovat role</Text>
<Group gap={6} wrap="wrap">{roleBadges}</Group>
{selectedRoles.length > 0 && (
<Button size="xs" variant="light" onClick={() => setSelectedRoles([])}>Reset</Button>
)}
</Stack>
);
})(),
filtering: selectedRoles.length > 0,
},
{
accessor: "account_type",
title: "Typ účtu",
width: "7%",
render: (row) =>
row.account_type ? (
<Badge color="gray" variant="light">
{
accountTypeDropdownOptions.find(opt => opt.value === row.account_type)?.label || row.account_type
}
</Badge>
) : (
<Text c="dimmed" fs="italic"></Text>
),
sortable: true,
filter: (() => {
const toggle = (val) => {
setSelectedAccountTypes(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val]);
};
return (
<Stack gap={4} style={{ minWidth: 170 }}>
<Text fw={500} size="xs" c="dimmed">Filtrovat typ účtu</Text>
<Group gap={6} wrap="wrap">
{accountTypeDropdownOptions.map(opt => {
const active = selectedAccountTypes.includes(opt.value);
return (
<Badge
key={opt.value}
color={active ? 'grape' : 'gray'}
variant={active ? 'filled' : 'outline'}
style={{ cursor:'pointer' }}
onClick={() => toggle(opt.value)}
aria-pressed={active}
role='button'
>
{opt.label}
</Badge>
);
})}
</Group>
{selectedAccountTypes.length > 0 && (
<Button size="xs" variant="light" onClick={() => setSelectedAccountTypes([])}>Reset</Button>
)}
</Stack>
);
})(),
filtering: selectedAccountTypes.length > 0,
},
{
accessor: "email_verified",
title: "E-mail ověřen",
width: "4%",
render: (row) =>
row.email_verified ? (
<Badge color="green" variant="light">Ano</Badge>
) : (
<Badge color="red" variant="light">Ne</Badge>
),
sortable: true,
filter: (() => {
const toggle = (val) => {
setSelectedEmailVerified(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val]);
};
const options = [
{ value: 'true', label: 'Ano', color: 'green' },
{ value: 'false', label: 'Ne', color: 'red' }
];
return (
<Stack gap={4} style={{ minWidth: 140 }}>
<Text fw={500} size="xs" c="dimmed">Ověření</Text>
<Group gap={6} wrap="wrap">
{options.map(opt => {
const active = selectedEmailVerified.includes(opt.value);
return (
<Badge
key={opt.value}
color={opt.color}
variant={active ? 'filled' : 'outline'}
style={{ cursor:'pointer' }}
onClick={() => toggle(opt.value)}
aria-pressed={active}
role='button'
>
{opt.label}
</Badge>
);
})}
</Group>
{selectedEmailVerified.length > 0 && (
<Button size="xs" variant="light" onClick={() => setSelectedEmailVerified([])}>Reset</Button>
)}
</Stack>
);
})(),
filtering: selectedEmailVerified.length > 0,
},
{
accessor: "is_active",
title: "Aktivní",
width: "4%",
render: (row) => row.is_active ? (
<Badge color="green" variant="light">Ano</Badge>
) : (
<Badge color="red" variant="light">Ne</Badge>
),
sortable: true,
filter: (() => {
const toggle = (val) => {
setSelectedActive(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val]);
};
const options = [
{ value: 'true', label: 'Ano', color: 'green' },
{ value: 'false', label: 'Ne', color: 'red' }
];
return (
<Stack gap={4} style={{ minWidth: 140 }}>
<Text fw={500} size="xs" c="dimmed">Aktivita</Text>
<Group gap={6} wrap="wrap">
{options.map(opt => {
const active = selectedActive.includes(opt.value);
return (
<Badge
key={opt.value}
color={opt.color}
variant={active ? 'filled' : 'outline'}
style={{ cursor:'pointer' }}
onClick={() => toggle(opt.value)}
aria-pressed={active}
role='button'
>
{opt.label}
</Badge>
);
})}
</Group>
{selectedActive.length > 0 && (
<Button size="xs" variant="light" onClick={() => setSelectedActive([])}>Reset</Button>
)}
</Stack>
);
})(),
filtering: selectedActive.length > 0,
},
{
accessor: "city",
title: "Město",
width: "5%",
filter: (
<TextInput
label="Filtrovat město"
placeholder="Zadej město"
leftSection={<IconSearch size={16} />}
rightSection={
<ActionIcon size="sm" variant="transparent" c="dimmed" onClick={() => setFilterCity("")}>
<IconX size={14} />
</ActionIcon>
}
value={filterCity}
onChange={(e) => setFilterCity(e.currentTarget.value)}
/>
),
filtering: !!filterCity,
},
{
accessor: "PSC",
title: "PSČ",
width: "3%",
filter: (
<TextInput
label="Filtrovat PSČ"
placeholder="Zadej PSČ"
leftSection={<IconSearch size={16} />}
rightSection={
<ActionIcon size="sm" variant="transparent" c="dimmed" onClick={() => setFilterPSC("")}>
<IconX size={14} />
</ActionIcon>
}
value={filterPSC}
onChange={(e) => setFilterPSC(e.currentTarget.value)}
/>
),
filtering: !!filterPSC,
},
{
accessor: "actions",
title: "Akce",
width: "3.5%",
render: (user) => (
<Group gap={4} wrap="nowrap">
<ActionIcon size="sm" variant="subtle" color="green" onClick={() => handleShowUser(user)}>
<IconEye size={16} />
</ActionIcon>
<ActionIcon size="sm" variant="subtle" color="blue" onClick={() => handleEditUser(user)}>
<IconEdit size={16} />
</ActionIcon>
<ActionIcon size="sm" variant="subtle" color="red" onClick={() => handleDeleteUser(user)}>
<IconTrash size={16} />
</ActionIcon>
</Group>
),
},
];
return (
<Container fluid className="p-0 d-flex flex-column" style={{ overflowX: "hidden", height: "100vh" }}>
<Row className="mx-0 flex-grow-1">
<Col xs={2} className="px-0 bg-light" style={{ minWidth: 0 }}>
<Sidebar />
</Col>
<Col xs={10} className="px-0 bg-white d-flex flex-column" style={{ minWidth: 0 }}>
<Group justify="space-between" align="center" px="md" py="sm">
<h1>
<IconReceipt2 size={30} style={{ marginRight: 10, marginTop: -4 }} />
Uživatelé
</h1>
<Button component="a" href="/manage/users/create" leftSection={<IconPlus size={16} />}>Vytvořit uživatele</Button>
</Group>
<Table
data={filteredUsers}
columns={columns}
fetching={fetching}
withTableBorder
borderRadius="md"
highlightOnHover
verticalAlign="center"
titlePadding="4px 8px"
/>
{/* Bootstrap Modal for view */}
<Modal show={showModal && modalType === 'view'} onHide={() => setShowModal(false)} centered>
<Modal.Header closeButton>
<Modal.Title>Detail uživatele</Modal.Title>
</Modal.Header>
<Modal.Body>
{selectedUser && (
<>
<p><strong>ID:</strong> {selectedUser.id}</p>
<p><strong>Uživatelské jméno:</strong> {selectedUser.username}</p>
<p><strong>Email:</strong> {selectedUser.email || ""}</p>
<p><strong>Jméno:</strong> {selectedUser.first_name || ""}</p>
<p><strong>Příjmení:</strong> {selectedUser.last_name || ""}</p>
<p><strong>Role:</strong> {(selectedUser.groups && selectedUser.groups.length > 0) ? selectedUser.groups.join(", ") : ""}</p>
<p><strong>Aktivní:</strong> {selectedUser.is_active ? "Ano" : "Ne"}</p>
</>
)}
</Modal.Body>
<Modal.Footer>
<BootstrapButton variant="secondary" onClick={() => setShowModal(false)}>Zavřít</BootstrapButton>
<BootstrapButton variant="primary" onClick={() => { setShowModal(false); handleEditUser(selectedUser); }}>Upravit</BootstrapButton>
</Modal.Footer>
</Modal>
{/* Bootstrap Modal for edit */}
<Modal show={showModal && modalType === 'edit'} onHide={() => setShowModal(false)} centered>
<Modal.Header closeButton>
<Modal.Title>Upravit uživatele</Modal.Title>
</Modal.Header>
<Form onSubmit={handleEditModalSubmit}>
<Modal.Body>
<Form.Group className="mb-3">
<Form.Label>Uživatelské jméno</Form.Label>
<Form.Control name="username" value={formData.username} onChange={handleChange} required />
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Email</Form.Label>
<Form.Control name="email" value={formData.email} onChange={handleChange} type="email" required />
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Jméno</Form.Label>
<Form.Control name="first_name" value={formData.first_name} onChange={handleChange} />
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Příjmení</Form.Label>
<Form.Control name="last_name" value={formData.last_name} onChange={handleChange} />
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Role</Form.Label>
<Form.Select name="role" value={formData.role} onChange={handleChange}>
<option value=""></option>
{roleDropdownOptions.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</Form.Select>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Typ účtu</Form.Label>
<Form.Select name="account_type" value={formData.account_type} onChange={handleChange}>
<option value=""></option>
{accountTypeDropdownOptions.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</Form.Select>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Telefon</Form.Label>
<Form.Control name="phone_number" value={formData.phone_number} onChange={handleChange} />
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Město</Form.Label>
<Form.Control name="city" value={formData.city} onChange={handleChange} />
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Ulice</Form.Label>
<Form.Control name="street" value={formData.street} onChange={handleChange} />
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>PSČ</Form.Label>
<Form.Control name="PSC" value={formData.PSC} onChange={handleChange} />
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Bankovní účet</Form.Label>
<Form.Control name="bank_account" value={formData.bank_account} onChange={handleChange} />
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>IČO</Form.Label>
<Form.Control name="ICO" value={formData.ICO} onChange={handleChange} />
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Rodné číslo</Form.Label>
<Form.Control name="RC" value={formData.RC} onChange={handleChange} />
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Variabilní symbol</Form.Label>
<Form.Control
name="var_symbol"
value={formData.var_symbol}
onChange={handleChange}
type="number"
min="0"
max="9999999999"
/>
</Form.Group>
<Form.Group className="mb-3">
<Form.Check
type="checkbox"
id="email_verified"
name="email_verified"
label="E-mail ověřen"
checked={formData.email_verified}
onChange={handleChange}
/>
</Form.Group>
<Form.Group className="mb-3">
<Form.Check
type="checkbox"
id="GDPR"
name="GDPR"
label="GDPR souhlas"
checked={formData.GDPR}
onChange={handleChange}
/>
</Form.Group>
<Form.Group className="mb-3">
<Form.Check
type="switch"
id="is_active"
name="is_active"
label={formData.is_active ? "Aktivní" : "Neaktivní"}
checked={formData.is_active}
onChange={e => setFormData(old => ({ ...old, is_active: e.target.checked }))}
/>
</Form.Group>
{error && <Alert variant="danger">{error}</Alert>}
</Modal.Body>
<Modal.Footer>
<BootstrapButton variant="secondary" onClick={() => setShowModal(false)}>Zrušit</BootstrapButton>
<BootstrapButton type="submit" variant="primary" disabled={submitting}>Uložit změny</BootstrapButton>
</Modal.Footer>
</Form>
</Modal>
</Col>
</Row>
</Container>
);
}
export default Users;