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 { useState } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import ConfirmModal from "../components/ConfirmModal";
|
import ConfirmModal from "../components/ConfirmModal";
|
||||||
|
import FormModal from "../components/FormModal";
|
||||||
|
import FormField from "../components/FormField";
|
||||||
|
|
||||||
import { formatDate, czechPlural } from "../utils/formatters";
|
import { formatDate, czechPlural } from "../utils/formatters";
|
||||||
import SortIcon from "../components/SortIcon";
|
import SortIcon from "../components/SortIcon";
|
||||||
import useTableSort from "../hooks/useTableSort";
|
import useTableSort from "../hooks/useTableSort";
|
||||||
import { usePaginatedQuery } from "../hooks/usePaginatedQuery";
|
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 { useApiMutation } from "../lib/queries/mutations";
|
||||||
import Pagination from "../components/Pagination";
|
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 {
|
const {
|
||||||
items: projects,
|
items: projects,
|
||||||
pagination,
|
pagination,
|
||||||
@@ -126,6 +197,27 @@ export default function Projects() {
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -172,7 +264,8 @@ export default function Projects() {
|
|||||||
fontSize: "0.875rem",
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -233,6 +326,7 @@ export default function Projects() {
|
|||||||
order={order}
|
order={order}
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
|
<th>Typ</th>
|
||||||
<th>Objednávka</th>
|
<th>Objednávka</th>
|
||||||
<th>Akce</th>
|
<th>Akce</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -257,6 +351,11 @@ export default function Projects() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="admin-mono">{formatDate(p.start_date)}</td>
|
<td className="admin-mono">{formatDate(p.start_date)}</td>
|
||||||
<td className="admin-mono">{formatDate(p.end_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>
|
<td>
|
||||||
{p.order_id ? (
|
{p.order_id ? (
|
||||||
<Link
|
<Link
|
||||||
@@ -359,6 +458,114 @@ export default function Projects() {
|
|||||||
type="danger"
|
type="danger"
|
||||||
loading={deleteMutation.isPending}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user