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:
@@ -7,6 +7,16 @@ function generateProjectNumber(PDO $pdo): string
|
||||
return generateSharedNumber($pdo);
|
||||
}
|
||||
|
||||
function handleGetUsers(PDO $pdo): void
|
||||
{
|
||||
$stmt = $pdo->query(
|
||||
"SELECT id, CONCAT(first_name, ' ', last_name) as name
|
||||
FROM users WHERE is_active = 1 ORDER BY first_name, last_name"
|
||||
);
|
||||
$users = $stmt->fetchAll();
|
||||
successResponse(['users' => $users]);
|
||||
}
|
||||
|
||||
function handleGetNextNumber(PDO $pdo): void
|
||||
{
|
||||
$number = generateProjectNumber($pdo);
|
||||
@@ -37,6 +47,15 @@ function handleCreateProject(PDO $pdo): void
|
||||
errorResponse('Zákazník nebyl nalezen', 404);
|
||||
}
|
||||
|
||||
$responsibleUserId = isset($input['responsible_user_id']) ? (int)$input['responsible_user_id'] : null;
|
||||
if ($responsibleUserId) {
|
||||
$stmt = $pdo->prepare('SELECT id FROM users WHERE id = ?');
|
||||
$stmt->execute([$responsibleUserId]);
|
||||
if (!$stmt->fetch()) {
|
||||
errorResponse('Zodpovědná osoba nebyla nalezena', 404);
|
||||
}
|
||||
}
|
||||
|
||||
$startDate = $input['start_date'] ?? date('Y-m-d');
|
||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) {
|
||||
errorResponse('Neplatný formát data zahájení');
|
||||
@@ -79,14 +98,15 @@ function handleCreateProject(PDO $pdo): void
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO projects (
|
||||
project_number, name, customer_id,
|
||||
project_number, name, customer_id, responsible_user_id,
|
||||
status, start_date, created_at, modified_at
|
||||
) VALUES (?, ?, ?, 'aktivni', ?, NOW(), NOW())
|
||||
) VALUES (?, ?, ?, ?, 'aktivni', ?, NOW(), NOW())
|
||||
");
|
||||
$stmt->execute([
|
||||
$projectNumber,
|
||||
$name,
|
||||
$customerId,
|
||||
$responsibleUserId,
|
||||
$startDate,
|
||||
]);
|
||||
$projectId = (int)$pdo->lastInsertId();
|
||||
@@ -185,15 +205,17 @@ function handleGetList(PDO $pdo): void
|
||||
|
||||
$from = "FROM projects p
|
||||
LEFT JOIN customers c ON p.customer_id = c.id
|
||||
LEFT JOIN orders o ON p.order_id = o.id";
|
||||
LEFT JOIN orders o ON p.order_id = o.id
|
||||
LEFT JOIN users u ON p.responsible_user_id = u.id";
|
||||
|
||||
$result = PaginationHelper::paginate(
|
||||
$pdo,
|
||||
"SELECT COUNT(*) {$from} {$where}",
|
||||
"SELECT p.id, p.project_number, p.name, p.status, p.start_date, p.end_date,
|
||||
p.order_id, p.quotation_id, p.created_at,
|
||||
p.order_id, p.quotation_id, p.created_at, p.responsible_user_id,
|
||||
c.name as customer_name,
|
||||
o.order_number
|
||||
o.order_number,
|
||||
CONCAT(u.first_name, ' ', u.last_name) as responsible_user_name
|
||||
{$from} {$where}
|
||||
ORDER BY {$p['sort']} {$p['order']}",
|
||||
$params,
|
||||
@@ -212,14 +234,17 @@ function handleGetDetail(PDO $pdo, int $id): void
|
||||
SELECT p.id, p.project_number, p.name, p.customer_id,
|
||||
p.quotation_id, p.order_id, p.status,
|
||||
p.start_date, p.end_date, p.notes,
|
||||
p.responsible_user_id,
|
||||
p.created_at, p.modified_at,
|
||||
c.name as customer_name,
|
||||
o.order_number, o.status as order_status,
|
||||
q.quotation_number
|
||||
q.quotation_number,
|
||||
CONCAT(u.first_name, \' \', u.last_name) as responsible_user_name
|
||||
FROM projects p
|
||||
LEFT JOIN customers c ON p.customer_id = c.id
|
||||
LEFT JOIN orders o ON p.order_id = o.id
|
||||
LEFT JOIN quotations q ON p.quotation_id = q.id
|
||||
LEFT JOIN users u ON p.responsible_user_id = u.id
|
||||
WHERE p.id = ?
|
||||
');
|
||||
$stmt->execute([$id]);
|
||||
@@ -235,7 +260,7 @@ function handleGetDetail(PDO $pdo, int $id): void
|
||||
function handleUpdateProject(PDO $pdo, int $id): void
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT id, project_number, name, status, start_date, end_date, notes
|
||||
'SELECT id, project_number, name, status, start_date, end_date, notes, responsible_user_id
|
||||
FROM projects WHERE id = ?'
|
||||
);
|
||||
$stmt->execute([$id]);
|
||||
@@ -282,6 +307,18 @@ function handleUpdateProject(PDO $pdo, int $id): void
|
||||
errorResponse('Poznámky jsou příliš dlouhé (max 5000 znaků)');
|
||||
}
|
||||
|
||||
// Zodpovedna osoba
|
||||
$responsibleUserId = array_key_exists('responsible_user_id', $input)
|
||||
? ($input['responsible_user_id'] ? (int)$input['responsible_user_id'] : null)
|
||||
: $project['responsible_user_id'];
|
||||
if ($responsibleUserId) {
|
||||
$stmt = $pdo->prepare('SELECT id FROM users WHERE id = ?');
|
||||
$stmt->execute([$responsibleUserId]);
|
||||
if (!$stmt->fetch()) {
|
||||
errorResponse('Zodpovědná osoba nebyla nalezena', 404);
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
$stmt = $pdo->prepare('
|
||||
@@ -291,6 +328,7 @@ function handleUpdateProject(PDO $pdo, int $id): void
|
||||
start_date = ?,
|
||||
end_date = ?,
|
||||
notes = ?,
|
||||
responsible_user_id = ?,
|
||||
modified_at = NOW()
|
||||
WHERE id = ?
|
||||
');
|
||||
@@ -300,6 +338,7 @@ function handleUpdateProject(PDO $pdo, int $id): void
|
||||
$input['start_date'] ?? $project['start_date'],
|
||||
$input['end_date'] ?? $project['end_date'],
|
||||
$notes,
|
||||
$responsibleUserId,
|
||||
$id,
|
||||
]);
|
||||
|
||||
|
||||
@@ -56,6 +56,9 @@ try {
|
||||
requirePermission($authData, 'projects.create');
|
||||
handleGetNextNumber($pdo);
|
||||
break;
|
||||
case 'users':
|
||||
handleGetUsers($pdo);
|
||||
break;
|
||||
default:
|
||||
handleGetList($pdo);
|
||||
}
|
||||
|
||||
53
dist/api/admin/handlers/projects-handlers.php
vendored
53
dist/api/admin/handlers/projects-handlers.php
vendored
@@ -7,6 +7,16 @@ function generateProjectNumber(PDO $pdo): string
|
||||
return generateSharedNumber($pdo);
|
||||
}
|
||||
|
||||
function handleGetUsers(PDO $pdo): void
|
||||
{
|
||||
$stmt = $pdo->query(
|
||||
"SELECT id, CONCAT(first_name, ' ', last_name) as name
|
||||
FROM users WHERE is_active = 1 ORDER BY first_name, last_name"
|
||||
);
|
||||
$users = $stmt->fetchAll();
|
||||
successResponse(['users' => $users]);
|
||||
}
|
||||
|
||||
function handleGetNextNumber(PDO $pdo): void
|
||||
{
|
||||
$number = generateProjectNumber($pdo);
|
||||
@@ -37,6 +47,15 @@ function handleCreateProject(PDO $pdo): void
|
||||
errorResponse('Zákazník nebyl nalezen', 404);
|
||||
}
|
||||
|
||||
$responsibleUserId = isset($input['responsible_user_id']) ? (int)$input['responsible_user_id'] : null;
|
||||
if ($responsibleUserId) {
|
||||
$stmt = $pdo->prepare('SELECT id FROM users WHERE id = ?');
|
||||
$stmt->execute([$responsibleUserId]);
|
||||
if (!$stmt->fetch()) {
|
||||
errorResponse('Zodpovědná osoba nebyla nalezena', 404);
|
||||
}
|
||||
}
|
||||
|
||||
$startDate = $input['start_date'] ?? date('Y-m-d');
|
||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) {
|
||||
errorResponse('Neplatný formát data zahájení');
|
||||
@@ -79,14 +98,15 @@ function handleCreateProject(PDO $pdo): void
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO projects (
|
||||
project_number, name, customer_id,
|
||||
project_number, name, customer_id, responsible_user_id,
|
||||
status, start_date, created_at, modified_at
|
||||
) VALUES (?, ?, ?, 'aktivni', ?, NOW(), NOW())
|
||||
) VALUES (?, ?, ?, ?, 'aktivni', ?, NOW(), NOW())
|
||||
");
|
||||
$stmt->execute([
|
||||
$projectNumber,
|
||||
$name,
|
||||
$customerId,
|
||||
$responsibleUserId,
|
||||
$startDate,
|
||||
]);
|
||||
$projectId = (int)$pdo->lastInsertId();
|
||||
@@ -185,15 +205,17 @@ function handleGetList(PDO $pdo): void
|
||||
|
||||
$from = "FROM projects p
|
||||
LEFT JOIN customers c ON p.customer_id = c.id
|
||||
LEFT JOIN orders o ON p.order_id = o.id";
|
||||
LEFT JOIN orders o ON p.order_id = o.id
|
||||
LEFT JOIN users u ON p.responsible_user_id = u.id";
|
||||
|
||||
$result = PaginationHelper::paginate(
|
||||
$pdo,
|
||||
"SELECT COUNT(*) {$from} {$where}",
|
||||
"SELECT p.id, p.project_number, p.name, p.status, p.start_date, p.end_date,
|
||||
p.order_id, p.quotation_id, p.created_at,
|
||||
p.order_id, p.quotation_id, p.created_at, p.responsible_user_id,
|
||||
c.name as customer_name,
|
||||
o.order_number
|
||||
o.order_number,
|
||||
CONCAT(u.first_name, ' ', u.last_name) as responsible_user_name
|
||||
{$from} {$where}
|
||||
ORDER BY {$p['sort']} {$p['order']}",
|
||||
$params,
|
||||
@@ -212,14 +234,17 @@ function handleGetDetail(PDO $pdo, int $id): void
|
||||
SELECT p.id, p.project_number, p.name, p.customer_id,
|
||||
p.quotation_id, p.order_id, p.status,
|
||||
p.start_date, p.end_date, p.notes,
|
||||
p.responsible_user_id,
|
||||
p.created_at, p.modified_at,
|
||||
c.name as customer_name,
|
||||
o.order_number, o.status as order_status,
|
||||
q.quotation_number
|
||||
q.quotation_number,
|
||||
CONCAT(u.first_name, \' \', u.last_name) as responsible_user_name
|
||||
FROM projects p
|
||||
LEFT JOIN customers c ON p.customer_id = c.id
|
||||
LEFT JOIN orders o ON p.order_id = o.id
|
||||
LEFT JOIN quotations q ON p.quotation_id = q.id
|
||||
LEFT JOIN users u ON p.responsible_user_id = u.id
|
||||
WHERE p.id = ?
|
||||
');
|
||||
$stmt->execute([$id]);
|
||||
@@ -235,7 +260,7 @@ function handleGetDetail(PDO $pdo, int $id): void
|
||||
function handleUpdateProject(PDO $pdo, int $id): void
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT id, project_number, name, status, start_date, end_date, notes
|
||||
'SELECT id, project_number, name, status, start_date, end_date, notes, responsible_user_id
|
||||
FROM projects WHERE id = ?'
|
||||
);
|
||||
$stmt->execute([$id]);
|
||||
@@ -282,6 +307,18 @@ function handleUpdateProject(PDO $pdo, int $id): void
|
||||
errorResponse('Poznámky jsou příliš dlouhé (max 5000 znaků)');
|
||||
}
|
||||
|
||||
// Zodpovedna osoba
|
||||
$responsibleUserId = array_key_exists('responsible_user_id', $input)
|
||||
? ($input['responsible_user_id'] ? (int)$input['responsible_user_id'] : null)
|
||||
: $project['responsible_user_id'];
|
||||
if ($responsibleUserId) {
|
||||
$stmt = $pdo->prepare('SELECT id FROM users WHERE id = ?');
|
||||
$stmt->execute([$responsibleUserId]);
|
||||
if (!$stmt->fetch()) {
|
||||
errorResponse('Zodpovědná osoba nebyla nalezena', 404);
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
$stmt = $pdo->prepare('
|
||||
@@ -291,6 +328,7 @@ function handleUpdateProject(PDO $pdo, int $id): void
|
||||
start_date = ?,
|
||||
end_date = ?,
|
||||
notes = ?,
|
||||
responsible_user_id = ?,
|
||||
modified_at = NOW()
|
||||
WHERE id = ?
|
||||
');
|
||||
@@ -300,6 +338,7 @@ function handleUpdateProject(PDO $pdo, int $id): void
|
||||
$input['start_date'] ?? $project['start_date'],
|
||||
$input['end_date'] ?? $project['end_date'],
|
||||
$notes,
|
||||
$responsibleUserId,
|
||||
$id,
|
||||
]);
|
||||
|
||||
|
||||
3
dist/api/admin/projects.php
vendored
3
dist/api/admin/projects.php
vendored
@@ -56,6 +56,9 @@ try {
|
||||
requirePermission($authData, 'projects.create');
|
||||
handleGetNextNumber($pdo);
|
||||
break;
|
||||
case 'users':
|
||||
handleGetUsers($pdo);
|
||||
break;
|
||||
default:
|
||||
handleGetList($pdo);
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"window_start":1773397693,"count":7}
|
||||
{"window_start":1773397817,"count":1}
|
||||
@@ -1 +1 @@
|
||||
{"window_start":1773396919,"count":1}
|
||||
{"window_start":1773399442,"count":1}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
import{j as e,m as f}from"./vendor-animation-0s3FMHwK.js";import{r as m}from"./vendor-react-BVs3cwbi.js";import{a9 as T}from"./vendor-utils-Dyr8OjFr.js";import{a as C,u as A,c as O,F as B,A as H}from"./index-CnEy_BDh.js";import{F as I}from"./Forbidden-D25jV3Oq.js";import{c as W,b as k,g as w,d as z,e as S,a as v,h as E,i as y,f as b}from"./attendanceHelpers-D6sLEw0q.js";const L="/api/admin",R=s=>s.break_start&&s.break_end?`${b(s.break_start)} - ${b(s.break_end)}`:s.break_start?`${b(s.break_start)} - ?`:"—",Z=s=>s.project_logs&&s.project_logs.length>0?e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"0.125rem"},children:s.project_logs.map((n,g)=>{let d,c,o=!1;if(n.hours!==null&&n.hours!==void 0)d=parseInt(n.hours)||0,c=parseInt(n.minutes)||0;else{o=!n.ended_at;const x=n.ended_at?new Date(n.ended_at):new Date,p=Math.floor((x-new Date(n.started_at))/6e4);d=Math.floor(p/60),c=p%60}return e.jsxs("span",{className:"admin-badge",style:{fontSize:"0.7rem",display:"inline-block",background:o?"var(--accent-light)":void 0},children:[n.project_name||`#${n.project_id}`," (",d,":",String(c).padStart(2,"0"),"h",o?" ▸":"",")"]},n.id||g)})}):s.project_name?e.jsx("span",{className:"admin-badge admin-badge-wrap",style:{fontSize:"0.75rem"},children:s.project_name}):"—",Y=s=>s.overtime>0?e.jsxs("span",{className:"leave-badge badge-overtime",children:["+",s.overtime,"h přesčas"]}):s.remaining>0?e.jsxs("span",{style:{color:"#dc2626"},children:["−",s.remaining,"h"]}):e.jsx("span",{style:{color:"#16a34a"},children:"splněno"});function Q(){const s=C(),{user:n,hasPermission:g}=A(),[d,c]=m.useState(!0),o=m.useRef(null),[x,p]=m.useState(()=>{const a=new Date;return`${a.getFullYear()}-${String(a.getMonth()+1).padStart(2,"0")}`}),[t,D]=m.useState({records:[],month_name:"",year:new Date().getFullYear(),total_minutes:0,vacation_hours:0,sick_hours:0,holiday_hours:0,unpaid_hours:0,leave_balance:null,monthly_fund:null}),_=m.useCallback(async()=>{c(!0);try{const a=await O(`${L}/attendance.php?action=history&month=${x}`);if(a.status===401)return;const i=await a.json();i.success&&D(i.data)}catch{s.error("Nepodařilo se načíst data")}finally{c(!1)}},[x,s]);if(m.useEffect(()=>{_()},[_]),!g("attendance.history"))return e.jsx(I,{});const $=()=>{if(!o.current)return;const a=window.open("","_blank");a.document.write(`
|
||||
import{j as e,m as f}from"./vendor-animation-0s3FMHwK.js";import{r as m}from"./vendor-react-BVs3cwbi.js";import{a9 as T}from"./vendor-utils-Dyr8OjFr.js";import{a as C,u as A,c as O,F as B,A as H}from"./index-CNxd7jIT.js";import{F as I}from"./Forbidden-D25jV3Oq.js";import{c as W,b as k,g as w,d as z,e as S,a as v,h as E,i as y,f as b}from"./attendanceHelpers-D6sLEw0q.js";const L="/api/admin",R=s=>s.break_start&&s.break_end?`${b(s.break_start)} - ${b(s.break_end)}`:s.break_start?`${b(s.break_start)} - ?`:"—",Z=s=>s.project_logs&&s.project_logs.length>0?e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"0.125rem"},children:s.project_logs.map((n,g)=>{let d,c,o=!1;if(n.hours!==null&&n.hours!==void 0)d=parseInt(n.hours)||0,c=parseInt(n.minutes)||0;else{o=!n.ended_at;const x=n.ended_at?new Date(n.ended_at):new Date,p=Math.floor((x-new Date(n.started_at))/6e4);d=Math.floor(p/60),c=p%60}return e.jsxs("span",{className:"admin-badge",style:{fontSize:"0.7rem",display:"inline-block",background:o?"var(--accent-light)":void 0},children:[n.project_name||`#${n.project_id}`," (",d,":",String(c).padStart(2,"0"),"h",o?" ▸":"",")"]},n.id||g)})}):s.project_name?e.jsx("span",{className:"admin-badge admin-badge-wrap",style:{fontSize:"0.75rem"},children:s.project_name}):"—",Y=s=>s.overtime>0?e.jsxs("span",{className:"leave-badge badge-overtime",children:["+",s.overtime,"h přesčas"]}):s.remaining>0?e.jsxs("span",{style:{color:"#dc2626"},children:["−",s.remaining,"h"]}):e.jsx("span",{style:{color:"#16a34a"},children:"splněno"});function Q(){const s=C(),{user:n,hasPermission:g}=A(),[d,c]=m.useState(!0),o=m.useRef(null),[x,p]=m.useState(()=>{const a=new Date;return`${a.getFullYear()}-${String(a.getMonth()+1).padStart(2,"0")}`}),[t,D]=m.useState({records:[],month_name:"",year:new Date().getFullYear(),total_minutes:0,vacation_hours:0,sick_hours:0,holiday_hours:0,unpaid_hours:0,leave_balance:null,monthly_fund:null}),_=m.useCallback(async()=>{c(!0);try{const a=await O(`${L}/attendance.php?action=history&month=${x}`);if(a.status===401)return;const i=await a.json();i.success&&D(i.data)}catch{s.error("Nepodařilo se načíst data")}finally{c(!1)}},[x,s]);if(m.useEffect(()=>{_()},[_]),!g("attendance.history"))return e.jsx(I,{});const $=()=>{if(!o.current)return;const a=window.open("","_blank");a.document.write(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="cs">
|
||||
<head>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
dist/assets/ProjectCreate-FGcLP7J1.js
vendored
Normal file
1
dist/assets/ProjectCreate-FGcLP7J1.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/ProjectCreate-k3l1K_1G.js
vendored
1
dist/assets/ProjectCreate-k3l1K_1G.js
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/ProjectDetail-CDPrOXSn.js
vendored
1
dist/assets/ProjectDetail-CDPrOXSn.js
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/ProjectDetail-Dg0G_KTk.js
vendored
Normal file
1
dist/assets/ProjectDetail-Dg0G_KTk.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
import{j as e,m as p,A as Z}from"./vendor-animation-0s3FMHwK.js";import{r as i,L as J}from"./vendor-react-BVs3cwbi.js";import{a9 as G}from"./vendor-utils-Dyr8OjFr.js";import{a as q,u as Q,c as b,b as X,F as r,A as C,f as l,C as ee}from"./index-CnEy_BDh.js";import{F as se}from"./Forbidden-D25jV3Oq.js";import{b as $}from"./attendanceHelpers-D6sLEw0q.js";const N="/api/admin";function de(){const d=q(),{hasPermission:L}=Q(),[k,D]=i.useState(!0),[j,V]=i.useState(()=>{const s=new Date;return`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-01`}),[g,A]=i.useState(()=>{const s=new Date,t=new Date(s.getFullYear(),s.getMonth()+1,0).getDate();return`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-${String(t).padStart(2,"0")}`}),[m,F]=i.useState(""),[h,E]=i.useState(""),[P,B]=i.useState({trips:[],vehicles:[],users:[],totals:{total:0,business:0,count:0}}),[n,I]=i.useState(null),w=i.useRef(null),[T,v]=i.useState(!1),[_,U]=i.useState(null),[a,o]=i.useState({vehicle_id:"",trip_date:"",start_km:"",end_km:"",route_from:"",route_to:"",is_business:1,notes:""}),[u,z]=i.useState({show:!1,trip:null}),y=i.useCallback(async(s=!0)=>{s&&D(!0);try{let t=`${N}/trips.php?action=admin&date_from=${j}&date_to=${g}`;m&&(t+=`&vehicle_id=${m}`),h&&(t+=`&user_id=${h}`);const c=await(await b(t)).json();c.success&&B(c.data)}catch{d.error("Nepodařilo se načíst data")}finally{s&&D(!1)}},[j,g,m,h,d]);if(i.useEffect(()=>{y()},[y]),X(T),!L("trips.admin"))return e.jsx(se,{});const H=s=>{U(s),o({vehicle_id:s.vehicle_id,trip_date:s.trip_date,start_km:s.start_km,end_km:s.end_km,route_from:s.route_from,route_to:s.route_to,is_business:s.is_business,notes:s.notes||""}),v(!0)},O=async()=>{if(parseInt(a.end_km)<=parseInt(a.start_km)){d.error("Konečný stav km musí být větší než počáteční");return}try{const t=await(await b(`${N}/trips.php?id=${_.id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)})).json();t.success?(v(!1),await y(!1),await new Promise(x=>setTimeout(x,300)),d.success(t.message)):d.error(t.error)}catch{d.error("Chyba připojení")}},W=async()=>{if(u.trip)try{const t=await(await b(`${N}/trips.php?id=${u.trip.id}`,{method:"DELETE"})).json();t.success?(z({show:!1,trip:null}),await y(!1),d.success(t.message)):d.error(t.error)}catch{d.error("Chyba připojení")}},K=async()=>{try{let s=`${N}/trips.php?action=print&date_from=${j}&date_to=${g}`;m&&(s+=`&vehicle_id=${m}`),h&&(s+=`&user_id=${h}`);const x=await(await b(s)).json();x.success&&(I(x.data),setTimeout(()=>{if(w.current){const c=window.open("","_blank");c.document.write(`
|
||||
import{j as e,m as p,A as Z}from"./vendor-animation-0s3FMHwK.js";import{r as i,L as J}from"./vendor-react-BVs3cwbi.js";import{a9 as G}from"./vendor-utils-Dyr8OjFr.js";import{a as q,u as Q,c as b,b as X,F as r,A as C,f as l,C as ee}from"./index-CNxd7jIT.js";import{F as se}from"./Forbidden-D25jV3Oq.js";import{b as $}from"./attendanceHelpers-D6sLEw0q.js";const N="/api/admin";function de(){const d=q(),{hasPermission:L}=Q(),[k,D]=i.useState(!0),[j,V]=i.useState(()=>{const s=new Date;return`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-01`}),[g,A]=i.useState(()=>{const s=new Date,t=new Date(s.getFullYear(),s.getMonth()+1,0).getDate();return`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-${String(t).padStart(2,"0")}`}),[m,F]=i.useState(""),[h,E]=i.useState(""),[P,B]=i.useState({trips:[],vehicles:[],users:[],totals:{total:0,business:0,count:0}}),[n,I]=i.useState(null),w=i.useRef(null),[T,v]=i.useState(!1),[_,U]=i.useState(null),[a,o]=i.useState({vehicle_id:"",trip_date:"",start_km:"",end_km:"",route_from:"",route_to:"",is_business:1,notes:""}),[u,z]=i.useState({show:!1,trip:null}),y=i.useCallback(async(s=!0)=>{s&&D(!0);try{let t=`${N}/trips.php?action=admin&date_from=${j}&date_to=${g}`;m&&(t+=`&vehicle_id=${m}`),h&&(t+=`&user_id=${h}`);const c=await(await b(t)).json();c.success&&B(c.data)}catch{d.error("Nepodařilo se načíst data")}finally{s&&D(!1)}},[j,g,m,h,d]);if(i.useEffect(()=>{y()},[y]),X(T),!L("trips.admin"))return e.jsx(se,{});const H=s=>{U(s),o({vehicle_id:s.vehicle_id,trip_date:s.trip_date,start_km:s.start_km,end_km:s.end_km,route_from:s.route_from,route_to:s.route_to,is_business:s.is_business,notes:s.notes||""}),v(!0)},O=async()=>{if(parseInt(a.end_km)<=parseInt(a.start_km)){d.error("Konečný stav km musí být větší než počáteční");return}try{const t=await(await b(`${N}/trips.php?id=${_.id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)})).json();t.success?(v(!1),await y(!1),await new Promise(x=>setTimeout(x,300)),d.success(t.message)):d.error(t.error)}catch{d.error("Chyba připojení")}},W=async()=>{if(u.trip)try{const t=await(await b(`${N}/trips.php?id=${u.trip.id}`,{method:"DELETE"})).json();t.success?(z({show:!1,trip:null}),await y(!1),d.success(t.message)):d.error(t.error)}catch{d.error("Chyba připojení")}},K=async()=>{try{let s=`${N}/trips.php?action=print&date_from=${j}&date_to=${g}`;m&&(s+=`&vehicle_id=${m}`),h&&(s+=`&user_id=${h}`);const x=await(await b(s)).json();x.success&&(I(x.data),setTimeout(()=>{if(w.current){const c=window.open("","_blank");c.document.write(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="cs">
|
||||
<head>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{j as x}from"./vendor-animation-0s3FMHwK.js";import{r as t}from"./vendor-react-BVs3cwbi.js";import{a as L,c as O}from"./index-CnEy_BDh.js";function J({column:e,sort:r,order:n}){return r!==e?null:x.jsx("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",style:{marginLeft:4,verticalAlign:"middle"},children:x.jsx("path",{d:n==="ASC"?"M18 15l-6-6-6 6":"M6 9l6 6 6-6"})})}function V(e,r="DESC"){const[n,a]=t.useState(e),[o,c]=t.useState(r),i=t.useRef(!1),S=t.useCallback(u=>{i.current=!0,a(m=>m===u?(c(h=>h==="ASC"?"DESC":"ASC"),m):(c("DESC"),u))},[]),d=i.current?n:null;return{sort:n,order:o,handleSort:S,activeSort:d}}function I(e,r=300){const[n,a]=t.useState(e);return t.useEffect(()=>{const o=setTimeout(()=>a(e),r);return()=>clearTimeout(o)},[e,r]),n}const N="/api/admin";function _(e,{dataKey:r,search:n,sort:a,order:o,page:c,perPage:i,extraParams:S,errorMsg:d="Nepodařilo se načíst data"}={}){const u=L(),[m,h]=t.useState([]),[j,D]=t.useState(!0),[w,k]=t.useState(null),l=t.useRef(null),p=S?JSON.stringify(S):"",b=I(n,300),C=t.useCallback(async()=>{l.current&&l.current.abort();const g=new AbortController;l.current=g;try{const s=new URLSearchParams;if(b&&s.set("search",b),a&&s.set("sort",a),o&&s.set("order",o),c&&s.set("page",c),i&&s.set("per_page",i),p){const R=JSON.parse(p);Object.entries(R).forEach(([y,A])=>{A&&s.set(y,A)})}const E=await O(`${N}/${e}?${s}`,{signal:g.signal});if(E.status===401)return;const f=await E.json();f.success?(h(f.data[r]||[]),f.data.pagination&&k(f.data.pagination)):u.error(f.error||d)}catch(s){if(s.name==="AbortError")return;u.error("Chyba připojení")}finally{D(!1)}},[u,e,r,b,a,o,c,i,p,d]);return t.useEffect(()=>(C(),()=>{l.current&&l.current.abort()}),[C]),{items:m,setItems:h,loading:j,pagination:w,refetch:C}}export{J as S,_ as a,V as u};
|
||||
import{j as x}from"./vendor-animation-0s3FMHwK.js";import{r as t}from"./vendor-react-BVs3cwbi.js";import{a as L,c as O}from"./index-CNxd7jIT.js";function J({column:e,sort:r,order:n}){return r!==e?null:x.jsx("svg",{width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",style:{marginLeft:4,verticalAlign:"middle"},children:x.jsx("path",{d:n==="ASC"?"M18 15l-6-6-6 6":"M6 9l6 6 6-6"})})}function V(e,r="DESC"){const[n,a]=t.useState(e),[o,c]=t.useState(r),i=t.useRef(!1),S=t.useCallback(u=>{i.current=!0,a(m=>m===u?(c(h=>h==="ASC"?"DESC":"ASC"),m):(c("DESC"),u))},[]),d=i.current?n:null;return{sort:n,order:o,handleSort:S,activeSort:d}}function I(e,r=300){const[n,a]=t.useState(e);return t.useEffect(()=>{const o=setTimeout(()=>a(e),r);return()=>clearTimeout(o)},[e,r]),n}const N="/api/admin";function _(e,{dataKey:r,search:n,sort:a,order:o,page:c,perPage:i,extraParams:S,errorMsg:d="Nepodařilo se načíst data"}={}){const u=L(),[m,h]=t.useState([]),[j,D]=t.useState(!0),[w,k]=t.useState(null),l=t.useRef(null),p=S?JSON.stringify(S):"",b=I(n,300),C=t.useCallback(async()=>{l.current&&l.current.abort();const g=new AbortController;l.current=g;try{const s=new URLSearchParams;if(b&&s.set("search",b),a&&s.set("sort",a),o&&s.set("order",o),c&&s.set("page",c),i&&s.set("per_page",i),p){const R=JSON.parse(p);Object.entries(R).forEach(([y,A])=>{A&&s.set(y,A)})}const E=await O(`${N}/${e}?${s}`,{signal:g.signal});if(E.status===401)return;const f=await E.json();f.success?(h(f.data[r]||[]),f.data.pagination&&k(f.data.pagination)):u.error(f.error||d)}catch(s){if(s.name==="AbortError")return;u.error("Chyba připojení")}finally{D(!1)}},[u,e,r,b,a,o,c,i,p,d]);return t.useEffect(()=>(C(),()=>{l.current&&l.current.abort()}),[C]),{items:m,setItems:h,loading:j,pagination:w,refetch:C}}export{J as S,_ as a,V as u};
|
||||
2
dist/index.html
vendored
2
dist/index.html
vendored
@@ -29,7 +29,7 @@
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Urbanist:wght@400;500;600;700;800&display=swap"
|
||||
rel="stylesheet" />
|
||||
<script type="module" crossorigin src="/assets/index-CnEy_BDh.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-CNxd7jIT.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/vendor-react-BVs3cwbi.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/vendor-animation-0s3FMHwK.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/vendor-utils-Dyr8OjFr.js">
|
||||
|
||||
4
dist/vendor/composer/installed.php
vendored
4
dist/vendor/composer/installed.php
vendored
@@ -3,7 +3,7 @@
|
||||
'name' => 'boha/website',
|
||||
'pretty_version' => 'dev-master',
|
||||
'version' => 'dev-master',
|
||||
'reference' => '10fbb9ebc7c5fecb2f704317c3024d5be8fc2d91',
|
||||
'reference' => '308941449e1b5f1ec6752c297d286e6633c11fed',
|
||||
'type' => 'project',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
@@ -13,7 +13,7 @@
|
||||
'boha/website' => array(
|
||||
'pretty_version' => 'dev-master',
|
||||
'version' => 'dev-master',
|
||||
'reference' => '10fbb9ebc7c5fecb2f704317c3024d5be8fc2d91',
|
||||
'reference' => '308941449e1b5f1ec6752c297d286e6633c11fed',
|
||||
'type' => 'project',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user