feat: zodpovedna osoba za projekt - novy sloupec + editace

Pridano pole responsible_user_id do tabulky projects s FK na users.
Select zodpovedne osoby v ProjectDetail, ProjectCreate a sloupec v seznamu projektu.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 12:03:44 +01:00
parent 308941449e
commit 9e3c95e576
44 changed files with 203 additions and 62 deletions

View File

@@ -20,8 +20,10 @@ export default function ProjectCreate() {
name: '',
customer_id: null,
customer_name: '',
start_date: new Date().toISOString().split('T')[0]
start_date: new Date().toISOString().split('T')[0],
responsible_user_id: ''
})
const [users, setUsers] = useState([])
const [saving, setSaving] = useState(false)
const [errors, setErrors] = useState({})
const [loadingNumber, setLoadingNumber] = useState(true)
@@ -35,9 +37,10 @@ export default function ProjectCreate() {
useEffect(() => {
const load = async () => {
try {
const [numRes, custRes] = await Promise.all([
const [numRes, custRes, usersRes] = await Promise.all([
apiFetch(`${API_BASE}/projects.php?action=next_number`),
apiFetch(`${API_BASE}/customers.php`)
apiFetch(`${API_BASE}/customers.php`),
apiFetch(`${API_BASE}/projects.php?action=users`)
])
const numData = await numRes.json()
@@ -49,6 +52,11 @@ export default function ProjectCreate() {
if (custData.success) {
setCustomers(custData.data.customers)
}
const usersData = await usersRes.json()
if (usersData.success) {
setUsers(usersData.data.users)
}
} catch {
alert.error('Chyba při načítání dat')
} finally {
@@ -109,7 +117,8 @@ export default function ProjectCreate() {
name: form.name.trim(),
customer_id: form.customer_id,
start_date: form.start_date,
project_number: form.project_number.trim()
project_number: form.project_number.trim(),
responsible_user_id: form.responsible_user_id || null
}
const res = await apiFetch(`${API_BASE}/projects.php`, {
@@ -263,6 +272,21 @@ export default function ProjectCreate() {
/>
</FormField>
</div>
<div className="admin-form-row">
<FormField label="Zodpovědná osoba">
<select
value={form.responsible_user_id}
onChange={(e) => updateForm('responsible_user_id', e.target.value)}
className="admin-form-select"
>
<option value=""> Nevybráno </option>
{users.map(u => (
<option key={u.id} value={u.id}>{u.name}</option>
))}
</select>
</FormField>
</div>
</div>
</div>
</motion.div>

View File

@@ -42,8 +42,10 @@ export default function ProjectDetail() {
name: '',
status: 'aktivni',
start_date: '',
end_date: ''
end_date: '',
responsible_user_id: ''
})
const [users, setUsers] = useState([])
const [deleteConfirm, setDeleteConfirm] = useState(false)
const [deleting, setDeleting] = useState(false)
@@ -91,7 +93,8 @@ export default function ProjectDetail() {
name: p.name || '',
status: p.status || 'aktivni',
start_date: (p.start_date || '').substring(0, 10),
end_date: (p.end_date || '').substring(0, 10)
end_date: (p.end_date || '').substring(0, 10),
responsible_user_id: p.responsible_user_id || ''
})
} else {
alert.error(result.error || 'Nepodařilo se načíst projekt')
@@ -104,8 +107,22 @@ export default function ProjectDetail() {
setLoading(false)
}
}
const fetchUsers = async () => {
try {
const res = await apiFetch(`${API_BASE}/projects.php?action=users`)
if (res.status === 401) return
const data = await res.json()
if (data.success) {
setUsers(data.data.users || [])
}
} catch {
// silent
}
}
fetchDetail()
fetchNotes()
fetchUsers()
}, [id, alert, navigate]) // eslint-disable-line react-hooks/exhaustive-deps
if (!hasPermission('projects.view')) return <Forbidden />
@@ -127,7 +144,8 @@ export default function ProjectDetail() {
name: form.name,
status: form.status,
start_date: form.start_date || null,
end_date: form.end_date || null
end_date: form.end_date || null,
responsible_user_id: form.responsible_user_id || null
})
})
const result = await response.json()
@@ -332,6 +350,22 @@ export default function ProjectDetail() {
style={{ backgroundColor: 'var(--bg-secondary)', cursor: 'default' }}
/>
</FormField>
<FormField label="Zodpovědná osoba">
<select
value={form.responsible_user_id}
onChange={(e) => updateForm('responsible_user_id', e.target.value)}
className="admin-form-select"
disabled={!canEdit}
>
<option value=""> Nevybráno </option>
{users.map(u => (
<option key={u.id} value={u.id}>{u.name}</option>
))}
</select>
</FormField>
</div>
<div className="admin-form-row admin-form-row-3">
<FormField label="Stav">
<select
value={form.status}
@@ -344,9 +378,6 @@ export default function ProjectDetail() {
<option value="zruseny">Zrušený</option>
</select>
</FormField>
</div>
<div className="admin-form-row">
<FormField label="Datum zahájení">
<AdminDatePicker
mode="date"

View File

@@ -189,6 +189,7 @@ export default function Projects() {
Název <SortIcon column="name" sort={activeSort} order={order} />
</th>
<th>Zákazník</th>
<th>Zodpovědná osoba</th>
<th style={{ cursor: 'pointer' }} onClick={() => handleSort('status')}>
Stav <SortIcon column="status" sort={activeSort} order={order} />
</th>
@@ -212,6 +213,7 @@ export default function Projects() {
</td>
<td className="fw-500">{p.name || '—'}</td>
<td>{p.customer_name || '—'}</td>
<td>{p.responsible_user_name || '—'}</td>
<td>
<span className={`admin-badge ${STATUS_CLASSES[p.status] || ''}`}>
{STATUS_LABELS[p.status] || p.status}