feat(projects-ui): add project create modal, type badge, empty-state copy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +1,24 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAlert } from "../context/AlertContext";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { Link } from "react-router-dom";
|
||||
import Forbidden from "../components/Forbidden";
|
||||
import { motion } from "framer-motion";
|
||||
import ConfirmModal from "../components/ConfirmModal";
|
||||
import FormModal from "../components/FormModal";
|
||||
import FormField from "../components/FormField";
|
||||
|
||||
import { formatDate, czechPlural } from "../utils/formatters";
|
||||
import SortIcon from "../components/SortIcon";
|
||||
import useTableSort from "../hooks/useTableSort";
|
||||
import { usePaginatedQuery } from "../hooks/usePaginatedQuery";
|
||||
import { projectListOptions } from "../lib/queries/projects";
|
||||
import {
|
||||
projectListOptions,
|
||||
projectNextNumberOptions,
|
||||
} from "../lib/queries/projects";
|
||||
import { offerCustomersOptions } from "../lib/queries/offers";
|
||||
import { userListOptions } from "../lib/queries/users";
|
||||
import { useApiMutation } from "../lib/queries/mutations";
|
||||
import Pagination from "../components/Pagination";
|
||||
|
||||
@@ -73,6 +81,69 @@ export default function Projects() {
|
||||
},
|
||||
});
|
||||
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
const [createForm, setCreateForm] = useState({
|
||||
name: "",
|
||||
customer_id: "",
|
||||
responsible_user_id: "",
|
||||
status: "aktivni",
|
||||
start_date: "",
|
||||
end_date: "",
|
||||
notes: "",
|
||||
});
|
||||
const [createErrors, setCreateErrors] = useState<Record<string, string>>({});
|
||||
const [creating, setCreating] = useState(false);
|
||||
|
||||
const customersQuery = useQuery({
|
||||
...offerCustomersOptions(),
|
||||
enabled: showCreate,
|
||||
});
|
||||
const usersQuery = useQuery({ ...userListOptions(), enabled: showCreate });
|
||||
const nextNumberQuery = useQuery({
|
||||
...projectNextNumberOptions(),
|
||||
enabled: showCreate,
|
||||
});
|
||||
|
||||
const createMutation = useApiMutation<typeof createForm, { id: number }>({
|
||||
url: () => `${API_BASE}/projects`,
|
||||
method: () => "POST",
|
||||
invalidate: ["projects", "orders", "offers", "invoices", "attendance"],
|
||||
onSuccess: () => {
|
||||
setShowCreate(false);
|
||||
alert.success("Projekt byl vytvořen");
|
||||
},
|
||||
});
|
||||
|
||||
const openCreate = () => {
|
||||
setCreateForm({
|
||||
name: "",
|
||||
customer_id: "",
|
||||
responsible_user_id: "",
|
||||
status: "aktivni",
|
||||
start_date: "",
|
||||
end_date: "",
|
||||
notes: "",
|
||||
});
|
||||
setCreateErrors({});
|
||||
setShowCreate(true);
|
||||
};
|
||||
|
||||
const handleCreate = async () => {
|
||||
const errs: Record<string, string> = {};
|
||||
if (!createForm.name.trim()) errs.name = "Zadejte název projektu";
|
||||
setCreateErrors(errs);
|
||||
if (Object.keys(errs).length > 0) return;
|
||||
|
||||
setCreating(true);
|
||||
try {
|
||||
await createMutation.mutateAsync(createForm);
|
||||
} catch (e) {
|
||||
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
items: projects,
|
||||
pagination,
|
||||
@@ -126,6 +197,27 @@ export default function Projects() {
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{hasPermission("projects.create") && (
|
||||
<div className="admin-page-actions">
|
||||
<button
|
||||
onClick={openCreate}
|
||||
className="admin-btn admin-btn-primary"
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
Přidat projekt
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
@@ -172,7 +264,8 @@ export default function Projects() {
|
||||
fontSize: "0.875rem",
|
||||
}}
|
||||
>
|
||||
Projekt se vytvoří automaticky při vytvoření objednávky.
|
||||
Projekty vznikají z objednávek nebo je můžete vytvořit ručně
|
||||
tlačítkem „Přidat projekt".
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -233,6 +326,7 @@ export default function Projects() {
|
||||
order={order}
|
||||
/>
|
||||
</th>
|
||||
<th>Typ</th>
|
||||
<th>Objednávka</th>
|
||||
<th>Akce</th>
|
||||
</tr>
|
||||
@@ -257,6 +351,11 @@ export default function Projects() {
|
||||
</td>
|
||||
<td className="admin-mono">{formatDate(p.start_date)}</td>
|
||||
<td className="admin-mono">{formatDate(p.end_date)}</td>
|
||||
<td>
|
||||
<span className="admin-badge">
|
||||
{p.order_id ? "Z objednávky" : "Samostatný"}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{p.order_id ? (
|
||||
<Link
|
||||
@@ -359,6 +458,114 @@ export default function Projects() {
|
||||
type="danger"
|
||||
loading={deleteMutation.isPending}
|
||||
/>
|
||||
|
||||
<FormModal
|
||||
isOpen={showCreate}
|
||||
onClose={() => setShowCreate(false)}
|
||||
onSubmit={handleCreate}
|
||||
title="Přidat projekt"
|
||||
submitLabel="Vytvořit"
|
||||
loading={creating}
|
||||
>
|
||||
<div className="admin-form">
|
||||
<FormField label="Název" error={createErrors.name} required>
|
||||
<input
|
||||
type="text"
|
||||
value={createForm.name}
|
||||
onChange={(e) => {
|
||||
setCreateForm({ ...createForm, name: e.target.value });
|
||||
setCreateErrors((p) => ({ ...p, name: "" }));
|
||||
}}
|
||||
className="admin-form-input"
|
||||
placeholder="Název projektu"
|
||||
aria-invalid={!!createErrors.name}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<div className="admin-form-row">
|
||||
<FormField label="Zákazník">
|
||||
<select
|
||||
value={createForm.customer_id}
|
||||
onChange={(e) =>
|
||||
setCreateForm({ ...createForm, customer_id: e.target.value })
|
||||
}
|
||||
className="admin-form-input"
|
||||
>
|
||||
<option value="">— bez zákazníka —</option>
|
||||
{(customersQuery.data ?? []).map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Zodpovědná osoba">
|
||||
<select
|
||||
value={createForm.responsible_user_id}
|
||||
onChange={(e) =>
|
||||
setCreateForm({
|
||||
...createForm,
|
||||
responsible_user_id: e.target.value,
|
||||
})
|
||||
}
|
||||
className="admin-form-input"
|
||||
>
|
||||
<option value="">— nepřiřazeno —</option>
|
||||
{(usersQuery.data ?? []).map((u) => (
|
||||
<option key={u.id} value={u.id}>
|
||||
{u.first_name} {u.last_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<div className="admin-form-row">
|
||||
<FormField label="Začátek">
|
||||
<input
|
||||
type="date"
|
||||
value={createForm.start_date}
|
||||
onChange={(e) =>
|
||||
setCreateForm({ ...createForm, start_date: e.target.value })
|
||||
}
|
||||
className="admin-form-input"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Konec">
|
||||
<input
|
||||
type="date"
|
||||
value={createForm.end_date}
|
||||
onChange={(e) =>
|
||||
setCreateForm({ ...createForm, end_date: e.target.value })
|
||||
}
|
||||
className="admin-form-input"
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<FormField label="Poznámka">
|
||||
<textarea
|
||||
value={createForm.notes}
|
||||
onChange={(e) =>
|
||||
setCreateForm({ ...createForm, notes: e.target.value })
|
||||
}
|
||||
className="admin-form-input"
|
||||
rows={3}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<p className="admin-form-hint">
|
||||
Číslo projektu:{" "}
|
||||
<strong>
|
||||
{nextNumberQuery.data?.number ??
|
||||
nextNumberQuery.data?.next_number ??
|
||||
"…"}
|
||||
</strong>{" "}
|
||||
(přiděleno automaticky)
|
||||
</p>
|
||||
</div>
|
||||
</FormModal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user