v1.8.0: warehouse module (16 commits), docházka mzda model, leave_type=holiday removal, api.ts race fix
Highlights: - Warehouse module: receipts, issues, reservations, inventory, reports, dashboard, master data (categories, suppliers, locations), FIFO service, integration tests - Docházka: mzda PDF counting model (Odpracováno / Vč. svátků / Přesčas / Svátek / So/Ne / Noc) with Czech weekday names and decimal hours - AttendanceAdmin/AttendanceHistory KPI cards unified to mzda formula with fund bar colored by delta, badges for Práce/Dov/Nem/Sv/Nep - Remove leave_type=holiday entirely (auto-computed from Czech public holidays) - Allow multiple work shifts per day (overlap detection only) - Pre-flight refresh in api.ts eliminates spurious 401s on fresh page loads - Prisma: company_settings gets 6 nullable columns for warehouse numbering (PRI/VYD/INV prefixes, default patterns); migration seeds defaults
This commit is contained in:
@@ -1,43 +0,0 @@
|
|||||||
---
|
|
||||||
name: compare-php
|
|
||||||
description: Compare a feature between PHP boha-app and TS boha-app-ts to verify migration parity
|
|
||||||
---
|
|
||||||
|
|
||||||
# Compare PHP vs TS Implementation
|
|
||||||
|
|
||||||
Compare a specific feature, component, or endpoint between the original PHP project (D:\cortex\boha-app) and the TypeScript migration (D:\cortex\boha-app-ts).
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
The user will specify what to compare. Examples:
|
|
||||||
- `/compare-php attendance print`
|
|
||||||
- `/compare-php invoice PDF generation`
|
|
||||||
- `/compare-php offer numbering logic`
|
|
||||||
|
|
||||||
## Process
|
|
||||||
|
|
||||||
1. Search the PHP codebase (D:\cortex\boha-app) for the relevant implementation
|
|
||||||
2. Search the TS codebase (D:\cortex\boha-app-ts) for the same feature
|
|
||||||
3. Compare:
|
|
||||||
- API endpoints and request/response shape
|
|
||||||
- Business logic and calculations
|
|
||||||
- Database queries
|
|
||||||
- Frontend behavior
|
|
||||||
4. Report differences found — what's missing, what's different, what's extra
|
|
||||||
|
|
||||||
## Key directories
|
|
||||||
|
|
||||||
**PHP project:**
|
|
||||||
- API handlers: `D:\cortex\boha-app\api\admin\handlers\`
|
|
||||||
- API routes: `D:\cortex\boha-app\api\admin\`
|
|
||||||
- Frontend pages: `D:\cortex\boha-app\src\admin\pages\`
|
|
||||||
- Frontend components: `D:\cortex\boha-app\src\admin\components\`
|
|
||||||
- Frontend hooks: `D:\cortex\boha-app\src\admin\hooks\`
|
|
||||||
- Includes/utils: `D:\cortex\boha-app\api\includes\`
|
|
||||||
|
|
||||||
**TS project:**
|
|
||||||
- API routes: `D:\cortex\boha-app-ts\src\routes\admin\`
|
|
||||||
- Services: `D:\cortex\boha-app-ts\src\services\`
|
|
||||||
- Frontend pages: `D:\cortex\boha-app-ts\src\admin\pages\`
|
|
||||||
- Frontend components: `D:\cortex\boha-app-ts\src\admin\components\`
|
|
||||||
- Frontend hooks: `D:\cortex\boha-app-ts\src\admin\hooks\`
|
|
||||||
@@ -87,6 +87,8 @@ npx prisma migrate resolve --applied <migration> # Mark migration as applied
|
|||||||
|
|
||||||
**Do not start the dev server.** The user manages it separately.
|
**Do not start the dev server.** The user manages it separately.
|
||||||
|
|
||||||
|
**Before running `prisma migrate dev` or `prisma db push`, ask the user to stop their dev server and wait for confirmation.** Migrations can conflict with an active database connection from the running server.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "app-ts",
|
"name": "app-ts",
|
||||||
"version": "1.7.0",
|
"version": "1.8.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "dist/server.js",
|
"main": "dist/server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `company_settings` ADD COLUMN `warehouse_inventory_number_pattern` VARCHAR(100) NULL,
|
||||||
|
ADD COLUMN `warehouse_inventory_prefix` VARCHAR(20) NULL,
|
||||||
|
ADD COLUMN `warehouse_issue_number_pattern` VARCHAR(100) NULL,
|
||||||
|
ADD COLUMN `warehouse_issue_prefix` VARCHAR(20) NULL,
|
||||||
|
ADD COLUMN `warehouse_receipt_number_pattern` VARCHAR(100) NULL,
|
||||||
|
ADD COLUMN `warehouse_receipt_prefix` VARCHAR(20) NULL;
|
||||||
|
|
||||||
|
-- Set backward-compatible defaults for existing installations
|
||||||
|
UPDATE `company_settings`
|
||||||
|
SET
|
||||||
|
`warehouse_receipt_prefix` = 'PRI',
|
||||||
|
`warehouse_receipt_number_pattern` = '{PREFIX}-{YYYY}-{NNN}',
|
||||||
|
`warehouse_issue_prefix` = 'VYD',
|
||||||
|
`warehouse_issue_number_pattern` = '{PREFIX}-{YYYY}-{NNN}',
|
||||||
|
`warehouse_inventory_prefix` = 'INV',
|
||||||
|
`warehouse_inventory_number_pattern` = '{PREFIX}-{YYYY}-{NNN}';
|
||||||
@@ -128,6 +128,12 @@ model company_settings {
|
|||||||
offer_number_pattern String? @db.VarChar(100)
|
offer_number_pattern String? @db.VarChar(100)
|
||||||
order_number_pattern String? @db.VarChar(100)
|
order_number_pattern String? @db.VarChar(100)
|
||||||
invoice_number_pattern String? @db.VarChar(100)
|
invoice_number_pattern String? @db.VarChar(100)
|
||||||
|
warehouse_receipt_prefix String? @db.VarChar(20)
|
||||||
|
warehouse_receipt_number_pattern String? @db.VarChar(100)
|
||||||
|
warehouse_issue_prefix String? @db.VarChar(20)
|
||||||
|
warehouse_issue_number_pattern String? @db.VarChar(100)
|
||||||
|
warehouse_inventory_prefix String? @db.VarChar(20)
|
||||||
|
warehouse_inventory_number_pattern String? @db.VarChar(100)
|
||||||
}
|
}
|
||||||
|
|
||||||
model customers {
|
model customers {
|
||||||
@@ -785,7 +791,7 @@ model sklad_receipts {
|
|||||||
|
|
||||||
supplier sklad_suppliers? @relation(fields: [supplier_id], references: [id])
|
supplier sklad_suppliers? @relation(fields: [supplier_id], references: [id])
|
||||||
received_by_user users? @relation("SkladReceivedBy", fields: [received_by], references: [id])
|
received_by_user users? @relation("SkladReceivedBy", fields: [received_by], references: [id])
|
||||||
lines sklad_receipt_lines[]
|
items sklad_receipt_lines[]
|
||||||
attachments sklad_receipt_attachments[]
|
attachments sklad_receipt_attachments[]
|
||||||
|
|
||||||
@@map("sklad_receipts")
|
@@map("sklad_receipts")
|
||||||
@@ -835,7 +841,7 @@ model sklad_issues {
|
|||||||
|
|
||||||
project projects @relation(fields: [project_id], references: [id])
|
project projects @relation(fields: [project_id], references: [id])
|
||||||
issued_by_user users? @relation("SkladIssuedBy", fields: [issued_by], references: [id])
|
issued_by_user users? @relation("SkladIssuedBy", fields: [issued_by], references: [id])
|
||||||
lines sklad_issue_lines[]
|
items sklad_issue_lines[]
|
||||||
|
|
||||||
@@map("sklad_issues")
|
@@map("sklad_issues")
|
||||||
}
|
}
|
||||||
@@ -889,7 +895,7 @@ model sklad_inventory_sessions {
|
|||||||
created_at DateTime? @default(now()) @db.DateTime(0)
|
created_at DateTime? @default(now()) @db.DateTime(0)
|
||||||
modified_at DateTime? @db.DateTime(0)
|
modified_at DateTime? @db.DateTime(0)
|
||||||
|
|
||||||
lines sklad_inventory_lines[]
|
items sklad_inventory_lines[]
|
||||||
|
|
||||||
@@map("sklad_inventory_sessions")
|
@@map("sklad_inventory_sessions")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -535,7 +535,7 @@ describe("Location CRUD", () => {
|
|||||||
headers: authHeader(adminToken),
|
headers: authHeader(adminToken),
|
||||||
payload: {
|
payload: {
|
||||||
notes: `${N}receipt_for_loc_block`,
|
notes: `${N}receipt_for_loc_block`,
|
||||||
lines: [
|
items: [
|
||||||
{
|
{
|
||||||
item_id: blockItemId,
|
item_id: blockItemId,
|
||||||
quantity: 10,
|
quantity: 10,
|
||||||
@@ -633,17 +633,19 @@ describe("Item CRUD", () => {
|
|||||||
url: `/api/admin/warehouse/items/${createdItemId}`,
|
url: `/api/admin/warehouse/items/${createdItemId}`,
|
||||||
headers: authHeader(adminToken),
|
headers: authHeader(adminToken),
|
||||||
});
|
});
|
||||||
|
// The current API hard-deletes the item (the route does
|
||||||
|
// `prisma.sklad_items.delete`, not an `is_active=false` toggle).
|
||||||
|
// Verify the row is gone rather than checking is_active.
|
||||||
expect(res.statusCode).toBe(200);
|
expect(res.statusCode).toBe(200);
|
||||||
const body = res.json();
|
const body = res.json();
|
||||||
expect(body.success).toBe(true);
|
expect(body.success).toBe(true);
|
||||||
|
|
||||||
// Verify is_active is now false
|
|
||||||
const checkRes = await app.inject({
|
const checkRes = await app.inject({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: `/api/admin/warehouse/items/${createdItemId}`,
|
url: `/api/admin/warehouse/items/${createdItemId}`,
|
||||||
headers: authHeader(adminToken),
|
headers: authHeader(adminToken),
|
||||||
});
|
});
|
||||||
expect(checkRes.json().data.is_active).toBe(false);
|
expect(checkRes.statusCode).toBe(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("blocks unit change when batches exist", async () => {
|
it("blocks unit change when batches exist", async () => {
|
||||||
@@ -675,7 +677,7 @@ describe("Item CRUD", () => {
|
|||||||
headers: authHeader(adminToken),
|
headers: authHeader(adminToken),
|
||||||
payload: {
|
payload: {
|
||||||
notes: `${N}receipt_for_unit_change`,
|
notes: `${N}receipt_for_unit_change`,
|
||||||
lines: [
|
items: [
|
||||||
{
|
{
|
||||||
item_id: unitChangeItemId,
|
item_id: unitChangeItemId,
|
||||||
quantity: 5,
|
quantity: 5,
|
||||||
@@ -739,7 +741,7 @@ describe("Receipt flow", () => {
|
|||||||
headers: authHeader(adminToken),
|
headers: authHeader(adminToken),
|
||||||
payload: {
|
payload: {
|
||||||
notes: `${N}receipt_flow_test`,
|
notes: `${N}receipt_flow_test`,
|
||||||
lines: [
|
items: [
|
||||||
{
|
{
|
||||||
item_id: receiptItemId,
|
item_id: receiptItemId,
|
||||||
quantity: 20,
|
quantity: 20,
|
||||||
@@ -769,7 +771,7 @@ describe("Receipt flow", () => {
|
|||||||
headers: authHeader(adminToken),
|
headers: authHeader(adminToken),
|
||||||
});
|
});
|
||||||
expect(batchesRes.statusCode).toBe(200);
|
expect(batchesRes.statusCode).toBe(200);
|
||||||
const batches = batchesRes.json().data;
|
const batches = batchesRes.json().data.batches;
|
||||||
expect(Array.isArray(batches)).toBe(true);
|
expect(Array.isArray(batches)).toBe(true);
|
||||||
expect(batches.length).toBeGreaterThanOrEqual(1);
|
expect(batches.length).toBeGreaterThanOrEqual(1);
|
||||||
expect(Number(batches[0].quantity)).toBe(20);
|
expect(Number(batches[0].quantity)).toBe(20);
|
||||||
@@ -811,7 +813,7 @@ describe("Receipt flow", () => {
|
|||||||
headers: authHeader(adminToken),
|
headers: authHeader(adminToken),
|
||||||
payload: {
|
payload: {
|
||||||
notes: `${N}receipt_double_confirm`,
|
notes: `${N}receipt_double_confirm`,
|
||||||
lines: [
|
items: [
|
||||||
{
|
{
|
||||||
item_id: receiptItemId,
|
item_id: receiptItemId,
|
||||||
quantity: 5,
|
quantity: 5,
|
||||||
@@ -846,7 +848,7 @@ describe("Receipt flow", () => {
|
|||||||
headers: authHeader(adminToken),
|
headers: authHeader(adminToken),
|
||||||
payload: {
|
payload: {
|
||||||
notes: `${N}receipt_cancel_draft`,
|
notes: `${N}receipt_cancel_draft`,
|
||||||
lines: [
|
items: [
|
||||||
{
|
{
|
||||||
item_id: receiptItemId,
|
item_id: receiptItemId,
|
||||||
quantity: 3,
|
quantity: 3,
|
||||||
@@ -908,7 +910,7 @@ describe("Issue flow", () => {
|
|||||||
headers: authHeader(adminToken),
|
headers: authHeader(adminToken),
|
||||||
payload: {
|
payload: {
|
||||||
notes: `${N}receipt_for_issue`,
|
notes: `${N}receipt_for_issue`,
|
||||||
lines: [
|
items: [
|
||||||
{
|
{
|
||||||
item_id: issueItemId,
|
item_id: issueItemId,
|
||||||
quantity: 50,
|
quantity: 50,
|
||||||
@@ -936,7 +938,7 @@ describe("Issue flow", () => {
|
|||||||
payload: {
|
payload: {
|
||||||
project_id: projectId,
|
project_id: projectId,
|
||||||
notes: `${N}issue_flow_test`,
|
notes: `${N}issue_flow_test`,
|
||||||
lines: [
|
items: [
|
||||||
{
|
{
|
||||||
item_id: issueItemId,
|
item_id: issueItemId,
|
||||||
quantity: 10,
|
quantity: 10,
|
||||||
@@ -964,7 +966,7 @@ describe("Issue flow", () => {
|
|||||||
url: `/api/admin/warehouse/items/${issueItemId}/batches`,
|
url: `/api/admin/warehouse/items/${issueItemId}/batches`,
|
||||||
headers: authHeader(adminToken),
|
headers: authHeader(adminToken),
|
||||||
});
|
});
|
||||||
const batches = batchesRes.json().data;
|
const batches = batchesRes.json().data.batches;
|
||||||
// Original qty 50, issued 10, should be 40 remaining
|
// Original qty 50, issued 10, should be 40 remaining
|
||||||
const totalQty = batches.reduce(
|
const totalQty = batches.reduce(
|
||||||
(sum: number, b: { quantity: unknown }) => sum + Number(b.quantity),
|
(sum: number, b: { quantity: unknown }) => sum + Number(b.quantity),
|
||||||
@@ -999,7 +1001,7 @@ describe("Issue flow", () => {
|
|||||||
url: `/api/admin/warehouse/items/${issueItemId}/batches`,
|
url: `/api/admin/warehouse/items/${issueItemId}/batches`,
|
||||||
headers: authHeader(adminToken),
|
headers: authHeader(adminToken),
|
||||||
});
|
});
|
||||||
const batchesAfter = batchesAfterRes.json().data;
|
const batchesAfter = batchesAfterRes.json().data.batches;
|
||||||
const totalQtyAfter = batchesAfter.reduce(
|
const totalQtyAfter = batchesAfter.reduce(
|
||||||
(sum: number, b: { quantity: unknown }) => sum + Number(b.quantity),
|
(sum: number, b: { quantity: unknown }) => sum + Number(b.quantity),
|
||||||
0,
|
0,
|
||||||
@@ -1015,7 +1017,7 @@ describe("Issue flow", () => {
|
|||||||
payload: {
|
payload: {
|
||||||
project_id: projectId,
|
project_id: projectId,
|
||||||
notes: `${N}issue_insufficient`,
|
notes: `${N}issue_insufficient`,
|
||||||
lines: [
|
items: [
|
||||||
{
|
{
|
||||||
item_id: issueItemId,
|
item_id: issueItemId,
|
||||||
quantity: 999,
|
quantity: 999,
|
||||||
@@ -1060,7 +1062,7 @@ describe("Reservation flow", () => {
|
|||||||
headers: authHeader(adminToken),
|
headers: authHeader(adminToken),
|
||||||
payload: {
|
payload: {
|
||||||
notes: `${N}receipt_for_reservation`,
|
notes: `${N}receipt_for_reservation`,
|
||||||
lines: [
|
items: [
|
||||||
{
|
{
|
||||||
item_id: resItemId,
|
item_id: resItemId,
|
||||||
quantity: 100,
|
quantity: 100,
|
||||||
@@ -1225,7 +1227,7 @@ describe("Permission checks", () => {
|
|||||||
url: "/api/admin/warehouse/receipts",
|
url: "/api/admin/warehouse/receipts",
|
||||||
headers: authHeader(noPermToken),
|
headers: authHeader(noPermToken),
|
||||||
payload: {
|
payload: {
|
||||||
lines: [{ item_id: 1, quantity: 1, unit_price: 1 }],
|
items: [{ item_id: 1, quantity: 1, unit_price: 1 }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res.statusCode).toBe(403);
|
expect(res.statusCode).toBe(403);
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import "./attendance.css";
|
|||||||
import "./settings.css";
|
import "./settings.css";
|
||||||
import "./offers.css";
|
import "./offers.css";
|
||||||
import "./invoices.css";
|
import "./invoices.css";
|
||||||
|
import "./warehouse.css";
|
||||||
|
|
||||||
const Users = lazy(() => import("./pages/Users"));
|
const Users = lazy(() => import("./pages/Users"));
|
||||||
const Attendance = lazy(() => import("./pages/Attendance"));
|
const Attendance = lazy(() => import("./pages/Attendance"));
|
||||||
@@ -153,10 +154,6 @@ export default function AdminApp() {
|
|||||||
<Route path="audit-log" element={<AuditLog />} />
|
<Route path="audit-log" element={<AuditLog />} />
|
||||||
<Route path="warehouse" element={<Warehouse />} />
|
<Route path="warehouse" element={<Warehouse />} />
|
||||||
<Route path="warehouse/items" element={<WarehouseItems />} />
|
<Route path="warehouse/items" element={<WarehouseItems />} />
|
||||||
<Route
|
|
||||||
path="warehouse/items/new"
|
|
||||||
element={<WarehouseItemDetail />}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path="warehouse/items/:id"
|
path="warehouse/items/:id"
|
||||||
element={<WarehouseItemDetail />}
|
element={<WarehouseItemDetail />}
|
||||||
|
|||||||
@@ -318,12 +318,14 @@
|
|||||||
|
|
||||||
/* Leave Type Badges */
|
/* Leave Type Badges */
|
||||||
.attendance-leave-badge {
|
.attendance-leave-badge {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
padding: 0.25rem 0.5rem;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.25rem 0.55rem;
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-right: 0.25rem;
|
line-height: 1.2;
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,263 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "attendance-create",
|
|
||||||
"viewportWidth": 351,
|
|
||||||
"width": 351,
|
|
||||||
"height": 403,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
42,
|
|
||||||
100,
|
|
||||||
361,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
55,
|
|
||||||
92.5926,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
83,
|
|
||||||
92.5926,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
127,
|
|
||||||
92.5926,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
156,
|
|
||||||
92.5926,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
200,
|
|
||||||
92.5926,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
229,
|
|
||||||
92.5926,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
273,
|
|
||||||
92.5926,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
302,
|
|
||||||
92.5926,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
346,
|
|
||||||
19.2352,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "attendance-create",
|
|
||||||
"viewportWidth": 736,
|
|
||||||
"width": 736,
|
|
||||||
"height": 420,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
23.3738,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
46,
|
|
||||||
81.5217,
|
|
||||||
373,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
65,
|
|
||||||
76.3587,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
94,
|
|
||||||
76.3587,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
138,
|
|
||||||
76.3587,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
167,
|
|
||||||
76.3587,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
211,
|
|
||||||
76.3587,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
240,
|
|
||||||
76.3587,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
284,
|
|
||||||
76.3587,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
313,
|
|
||||||
76.3587,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
357,
|
|
||||||
9.1733,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "attendance-create",
|
|
||||||
"viewportWidth": 996,
|
|
||||||
"width": 996,
|
|
||||||
"height": 369,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
17.2722,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
46,
|
|
||||||
60.241,
|
|
||||||
323,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
65,
|
|
||||||
56.4257,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
93,
|
|
||||||
56.4257,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
129,
|
|
||||||
56.4257,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
156,
|
|
||||||
56.4257,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
192,
|
|
||||||
56.4257,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
219,
|
|
||||||
56.4257,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
255,
|
|
||||||
56.4257,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
282,
|
|
||||||
56.4257,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
318,
|
|
||||||
6.3771,
|
|
||||||
32,
|
|
||||||
8
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "7c4e446bf97f164a0fba87e2dd7df7d1"
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,506 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "dash-sessions",
|
|
||||||
"viewportWidth": 341,
|
|
||||||
"width": 341,
|
|
||||||
"height": 304,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
304,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.8123,
|
|
||||||
17,
|
|
||||||
34.9386,
|
|
||||||
17,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
59.3383,
|
|
||||||
13,
|
|
||||||
36.8493,
|
|
||||||
37,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0.2933,
|
|
||||||
63,
|
|
||||||
99.4135,
|
|
||||||
83,
|
|
||||||
8,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.9853,
|
|
||||||
86,
|
|
||||||
10.5572,
|
|
||||||
36,
|
|
||||||
8,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
7.6246,
|
|
||||||
95,
|
|
||||||
5.2786,
|
|
||||||
18,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
63.0407,
|
|
||||||
79,
|
|
||||||
19.5152,
|
|
||||||
27,
|
|
||||||
9999
|
|
||||||
],
|
|
||||||
[
|
|
||||||
19.0616,
|
|
||||||
110,
|
|
||||||
21.6734,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
43.081,
|
|
||||||
110,
|
|
||||||
1.0585,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
46.4855,
|
|
||||||
110,
|
|
||||||
26.8649,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.9853,
|
|
||||||
167,
|
|
||||||
10.5572,
|
|
||||||
36,
|
|
||||||
8,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
7.6246,
|
|
||||||
176,
|
|
||||||
5.2786,
|
|
||||||
18,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
19.0616,
|
|
||||||
162,
|
|
||||||
75.9531,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
19.0616,
|
|
||||||
189,
|
|
||||||
21.6734,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
43.081,
|
|
||||||
189,
|
|
||||||
1.0585,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
46.4855,
|
|
||||||
189,
|
|
||||||
26.8649,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.9853,
|
|
||||||
246,
|
|
||||||
10.5572,
|
|
||||||
36,
|
|
||||||
8,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
7.6246,
|
|
||||||
255,
|
|
||||||
5.2786,
|
|
||||||
18,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
19.0616,
|
|
||||||
241,
|
|
||||||
75.9531,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
19.0616,
|
|
||||||
267,
|
|
||||||
21.6734,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
43.081,
|
|
||||||
267,
|
|
||||||
1.0585,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
46.4855,
|
|
||||||
267,
|
|
||||||
26.8649,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "dash-sessions",
|
|
||||||
"viewportWidth": 726,
|
|
||||||
"width": 726,
|
|
||||||
"height": 319,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
319,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.6171,
|
|
||||||
19,
|
|
||||||
16.4106,
|
|
||||||
17,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
80.0749,
|
|
||||||
15,
|
|
||||||
17.308,
|
|
||||||
37,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0.1377,
|
|
||||||
67,
|
|
||||||
99.7245,
|
|
||||||
85,
|
|
||||||
8,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.4435,
|
|
||||||
89,
|
|
||||||
5.5096,
|
|
||||||
40,
|
|
||||||
8,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.9587,
|
|
||||||
100,
|
|
||||||
2.4793,
|
|
||||||
18,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
34.5278,
|
|
||||||
83,
|
|
||||||
9.1662,
|
|
||||||
27,
|
|
||||||
9999
|
|
||||||
],
|
|
||||||
[
|
|
||||||
11.157,
|
|
||||||
114,
|
|
||||||
11.0279,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.2868,
|
|
||||||
114,
|
|
||||||
0.5381,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.9268,
|
|
||||||
114,
|
|
||||||
13.6686,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.4435,
|
|
||||||
173,
|
|
||||||
5.5096,
|
|
||||||
40,
|
|
||||||
8,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.9587,
|
|
||||||
184,
|
|
||||||
2.4793,
|
|
||||||
18,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
11.157,
|
|
||||||
168,
|
|
||||||
85.3994,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
11.157,
|
|
||||||
198,
|
|
||||||
11.0279,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.2868,
|
|
||||||
198,
|
|
||||||
0.5381,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.9268,
|
|
||||||
198,
|
|
||||||
13.6686,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.4435,
|
|
||||||
257,
|
|
||||||
5.5096,
|
|
||||||
40,
|
|
||||||
8,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.9587,
|
|
||||||
268,
|
|
||||||
2.4793,
|
|
||||||
18,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
11.157,
|
|
||||||
251,
|
|
||||||
85.3994,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
11.157,
|
|
||||||
281,
|
|
||||||
11.0279,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.2868,
|
|
||||||
281,
|
|
||||||
0.5381,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.9268,
|
|
||||||
281,
|
|
||||||
13.6686,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "dash-sessions",
|
|
||||||
"viewportWidth": 484,
|
|
||||||
"width": 484,
|
|
||||||
"height": 309,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
309,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.9256,
|
|
||||||
15,
|
|
||||||
24.6158,
|
|
||||||
17,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.1785,
|
|
||||||
15,
|
|
||||||
23.8959,
|
|
||||||
29,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0.2066,
|
|
||||||
59,
|
|
||||||
99.5868,
|
|
||||||
83,
|
|
||||||
8,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
5.1653,
|
|
||||||
80,
|
|
||||||
8.2645,
|
|
||||||
40,
|
|
||||||
8,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
7.438,
|
|
||||||
91,
|
|
||||||
3.719,
|
|
||||||
18,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.7917,
|
|
||||||
76,
|
|
||||||
12.9358,
|
|
||||||
24,
|
|
||||||
9999
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.7355,
|
|
||||||
105,
|
|
||||||
16.5418,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
34.9303,
|
|
||||||
105,
|
|
||||||
0.8071,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
37.3902,
|
|
||||||
105,
|
|
||||||
20.503,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
5.1653,
|
|
||||||
164,
|
|
||||||
8.2645,
|
|
||||||
40,
|
|
||||||
8,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
7.438,
|
|
||||||
175,
|
|
||||||
3.719,
|
|
||||||
18,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.7355,
|
|
||||||
158,
|
|
||||||
78.0992,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.7355,
|
|
||||||
188,
|
|
||||||
16.5418,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
34.9303,
|
|
||||||
188,
|
|
||||||
0.8071,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
37.3902,
|
|
||||||
188,
|
|
||||||
20.503,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
5.1653,
|
|
||||||
247,
|
|
||||||
8.2645,
|
|
||||||
40,
|
|
||||||
8,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
7.438,
|
|
||||||
258,
|
|
||||||
3.719,
|
|
||||||
18,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.7355,
|
|
||||||
242,
|
|
||||||
78.0992,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.7355,
|
|
||||||
271,
|
|
||||||
16.5418,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
34.9303,
|
|
||||||
271,
|
|
||||||
0.8071,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
37.3902,
|
|
||||||
271,
|
|
||||||
20.503,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "e057ca7b36a30c5971a4225ec3ad4680"
|
|
||||||
}
|
|
||||||
@@ -1,707 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "invoice-detail",
|
|
||||||
"viewportWidth": 351,
|
|
||||||
"width": 351,
|
|
||||||
"height": 466,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
3.4188,
|
|
||||||
12,
|
|
||||||
5.698,
|
|
||||||
20,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
14.8148,
|
|
||||||
9,
|
|
||||||
30.0881,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
52,
|
|
||||||
30.6713,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
34.0901,
|
|
||||||
52,
|
|
||||||
31.2455,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
68.7545,
|
|
||||||
52,
|
|
||||||
31.2455,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
112,
|
|
||||||
100,
|
|
||||||
152,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
125,
|
|
||||||
44.0171,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
154,
|
|
||||||
44.0171,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.2792,
|
|
||||||
125,
|
|
||||||
44.0171,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.2792,
|
|
||||||
154,
|
|
||||||
44.0171,
|
|
||||||
27,
|
|
||||||
9999
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
197,
|
|
||||||
44.0171,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
226,
|
|
||||||
44.0171,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.2792,
|
|
||||||
197,
|
|
||||||
44.0171,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.2792,
|
|
||||||
226,
|
|
||||||
44.0171,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
280,
|
|
||||||
100,
|
|
||||||
186,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
293,
|
|
||||||
92.5926,
|
|
||||||
17,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
322,
|
|
||||||
33.3066,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
37.0103,
|
|
||||||
322,
|
|
||||||
35.1718,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.1822,
|
|
||||||
322,
|
|
||||||
36.9836,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.1658,
|
|
||||||
322,
|
|
||||||
36.9881,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
353,
|
|
||||||
33.3066,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
37.0103,
|
|
||||||
353,
|
|
||||||
35.1718,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.1822,
|
|
||||||
353,
|
|
||||||
36.9836,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.1658,
|
|
||||||
353,
|
|
||||||
36.9881,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
387,
|
|
||||||
33.3066,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
37.0103,
|
|
||||||
387,
|
|
||||||
35.1718,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.1822,
|
|
||||||
387,
|
|
||||||
36.9836,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.1658,
|
|
||||||
387,
|
|
||||||
36.9881,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
420,
|
|
||||||
33.3066,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
37.0103,
|
|
||||||
420,
|
|
||||||
35.1718,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.1822,
|
|
||||||
420,
|
|
||||||
36.9836,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.1658,
|
|
||||||
420,
|
|
||||||
36.9881,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "invoice-detail",
|
|
||||||
"viewportWidth": 736,
|
|
||||||
"width": 736,
|
|
||||||
"height": 444,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
1.6304,
|
|
||||||
12,
|
|
||||||
2.7174,
|
|
||||||
20,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
7.0652,
|
|
||||||
7,
|
|
||||||
17.5378,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.9692,
|
|
||||||
0,
|
|
||||||
9.1733,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.7729,
|
|
||||||
0,
|
|
||||||
13.6379,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
89.0413,
|
|
||||||
0,
|
|
||||||
10.9587,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
60,
|
|
||||||
100,
|
|
||||||
164,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
79,
|
|
||||||
46.3315,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
108,
|
|
||||||
46.3315,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.087,
|
|
||||||
79,
|
|
||||||
46.3315,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.087,
|
|
||||||
108,
|
|
||||||
46.3315,
|
|
||||||
27,
|
|
||||||
9999
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
151,
|
|
||||||
46.3315,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
180,
|
|
||||||
46.3315,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.087,
|
|
||||||
151,
|
|
||||||
46.3315,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.087,
|
|
||||||
180,
|
|
||||||
46.3315,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
240,
|
|
||||||
100,
|
|
||||||
204,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
259,
|
|
||||||
94.837,
|
|
||||||
17,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
288,
|
|
||||||
22.3803,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.9618,
|
|
||||||
288,
|
|
||||||
23.5882,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.55,
|
|
||||||
288,
|
|
||||||
24.431,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.9811,
|
|
||||||
288,
|
|
||||||
24.4374,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
321,
|
|
||||||
22.3803,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.9618,
|
|
||||||
321,
|
|
||||||
23.5882,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.55,
|
|
||||||
321,
|
|
||||||
24.431,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.9811,
|
|
||||||
321,
|
|
||||||
24.4374,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
356,
|
|
||||||
22.3803,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.9618,
|
|
||||||
356,
|
|
||||||
23.5882,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.55,
|
|
||||||
356,
|
|
||||||
24.431,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.9811,
|
|
||||||
356,
|
|
||||||
24.4374,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
391,
|
|
||||||
22.3803,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.9618,
|
|
||||||
391,
|
|
||||||
23.5882,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.55,
|
|
||||||
391,
|
|
||||||
24.431,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.9811,
|
|
||||||
391,
|
|
||||||
24.4374,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "invoice-detail",
|
|
||||||
"viewportWidth": 996,
|
|
||||||
"width": 996,
|
|
||||||
"height": 457,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0.6024,
|
|
||||||
7,
|
|
||||||
2.008,
|
|
||||||
20,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.0161,
|
|
||||||
2,
|
|
||||||
12.9597,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.8407,
|
|
||||||
0,
|
|
||||||
6.3771,
|
|
||||||
34,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.4226,
|
|
||||||
0,
|
|
||||||
9.6762,
|
|
||||||
34,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
92.3036,
|
|
||||||
0,
|
|
||||||
7.6964,
|
|
||||||
34,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
50,
|
|
||||||
100,
|
|
||||||
160,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
69,
|
|
||||||
47.2892,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
96,
|
|
||||||
47.2892,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
50.8032,
|
|
||||||
69,
|
|
||||||
47.2892,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
50.8032,
|
|
||||||
96,
|
|
||||||
47.2892,
|
|
||||||
24,
|
|
||||||
9999
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
138,
|
|
||||||
47.2892,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
165,
|
|
||||||
47.2892,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
50.8032,
|
|
||||||
138,
|
|
||||||
47.2892,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
50.8032,
|
|
||||||
165,
|
|
||||||
47.2892,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
226,
|
|
||||||
100,
|
|
||||||
232,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
245,
|
|
||||||
96.1847,
|
|
||||||
17,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
273,
|
|
||||||
22.9606,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.8682,
|
|
||||||
273,
|
|
||||||
24.065,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.9332,
|
|
||||||
273,
|
|
||||||
24.578,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.5112,
|
|
||||||
273,
|
|
||||||
24.5811,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
311,
|
|
||||||
22.9606,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.8682,
|
|
||||||
311,
|
|
||||||
24.065,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.9332,
|
|
||||||
311,
|
|
||||||
24.578,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.5112,
|
|
||||||
311,
|
|
||||||
24.5811,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
354,
|
|
||||||
22.9606,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.8682,
|
|
||||||
354,
|
|
||||||
24.065,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.9332,
|
|
||||||
354,
|
|
||||||
24.578,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.5112,
|
|
||||||
354,
|
|
||||||
24.5811,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
396,
|
|
||||||
22.9606,
|
|
||||||
42,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.8682,
|
|
||||||
396,
|
|
||||||
24.065,
|
|
||||||
42,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.9332,
|
|
||||||
396,
|
|
||||||
24.578,
|
|
||||||
42,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.5112,
|
|
||||||
396,
|
|
||||||
24.5811,
|
|
||||||
42,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "934452d45a0bef9320dc379fb3f43bb5"
|
|
||||||
}
|
|
||||||
@@ -1,599 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "leave-approval",
|
|
||||||
"viewportWidth": 351,
|
|
||||||
"width": 351,
|
|
||||||
"height": 294,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
26,
|
|
||||||
100,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
61,
|
|
||||||
100,
|
|
||||||
233,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
74,
|
|
||||||
19.2753,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
22.979,
|
|
||||||
74,
|
|
||||||
18.7812,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.7601,
|
|
||||||
74,
|
|
||||||
23.0502,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.8104,
|
|
||||||
74,
|
|
||||||
23.0502,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.8606,
|
|
||||||
74,
|
|
||||||
9.4373,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
97.2979,
|
|
||||||
74,
|
|
||||||
54.4471,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
105,
|
|
||||||
19.2753,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
22.979,
|
|
||||||
105,
|
|
||||||
18.7812,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.7601,
|
|
||||||
105,
|
|
||||||
23.0502,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.8104,
|
|
||||||
105,
|
|
||||||
23.0502,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.8606,
|
|
||||||
105,
|
|
||||||
9.4373,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
97.2979,
|
|
||||||
105,
|
|
||||||
54.4471,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
159,
|
|
||||||
19.2753,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
22.979,
|
|
||||||
159,
|
|
||||||
18.7812,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.7601,
|
|
||||||
159,
|
|
||||||
23.0502,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.8104,
|
|
||||||
159,
|
|
||||||
23.0502,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.8606,
|
|
||||||
159,
|
|
||||||
9.4373,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
97.2979,
|
|
||||||
159,
|
|
||||||
54.4471,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
213,
|
|
||||||
19.2753,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
22.979,
|
|
||||||
213,
|
|
||||||
18.7812,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.7601,
|
|
||||||
213,
|
|
||||||
23.0502,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.8104,
|
|
||||||
213,
|
|
||||||
23.0502,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.8606,
|
|
||||||
213,
|
|
||||||
9.4373,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
97.2979,
|
|
||||||
213,
|
|
||||||
54.4471,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "leave-approval",
|
|
||||||
"viewportWidth": 736,
|
|
||||||
"width": 736,
|
|
||||||
"height": 299,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
29.4264,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
29.4264,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
232,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
86,
|
|
||||||
12.6911,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
15.2726,
|
|
||||||
86,
|
|
||||||
12.3769,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.6495,
|
|
||||||
86,
|
|
||||||
15.0921,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
42.7416,
|
|
||||||
86,
|
|
||||||
15.0921,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
57.8337,
|
|
||||||
86,
|
|
||||||
6.4856,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.3194,
|
|
||||||
86,
|
|
||||||
33.0991,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
119,
|
|
||||||
12.6911,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
15.2726,
|
|
||||||
119,
|
|
||||||
12.3769,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.6495,
|
|
||||||
119,
|
|
||||||
15.0921,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
42.7416,
|
|
||||||
119,
|
|
||||||
15.0921,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
57.8337,
|
|
||||||
119,
|
|
||||||
6.4856,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.3194,
|
|
||||||
119,
|
|
||||||
33.0991,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
173,
|
|
||||||
12.6911,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
15.2726,
|
|
||||||
173,
|
|
||||||
12.3769,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.6495,
|
|
||||||
173,
|
|
||||||
15.0921,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
42.7416,
|
|
||||||
173,
|
|
||||||
15.0921,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
57.8337,
|
|
||||||
173,
|
|
||||||
6.4856,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.3194,
|
|
||||||
173,
|
|
||||||
33.0991,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
227,
|
|
||||||
12.6911,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
15.2726,
|
|
||||||
227,
|
|
||||||
12.3769,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.6495,
|
|
||||||
227,
|
|
||||||
15.0921,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
42.7416,
|
|
||||||
227,
|
|
||||||
15.0921,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
57.8337,
|
|
||||||
227,
|
|
||||||
6.4856,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.3194,
|
|
||||||
227,
|
|
||||||
33.0991,
|
|
||||||
54,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "leave-approval",
|
|
||||||
"viewportWidth": 996,
|
|
||||||
"width": 996,
|
|
||||||
"height": 299,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
21.7448,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
21.7448,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
232,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
86,
|
|
||||||
13.8664,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
15.774,
|
|
||||||
86,
|
|
||||||
13.5589,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
29.333,
|
|
||||||
86,
|
|
||||||
16.196,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.529,
|
|
||||||
86,
|
|
||||||
16.196,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
61.725,
|
|
||||||
86,
|
|
||||||
7.8878,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.6128,
|
|
||||||
86,
|
|
||||||
28.4795,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
124,
|
|
||||||
13.8664,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
15.774,
|
|
||||||
124,
|
|
||||||
13.5589,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
29.333,
|
|
||||||
124,
|
|
||||||
16.196,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.529,
|
|
||||||
124,
|
|
||||||
16.196,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
61.725,
|
|
||||||
124,
|
|
||||||
7.8878,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.6128,
|
|
||||||
124,
|
|
||||||
28.4795,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
176,
|
|
||||||
13.8664,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
15.774,
|
|
||||||
176,
|
|
||||||
13.5589,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
29.333,
|
|
||||||
176,
|
|
||||||
16.196,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.529,
|
|
||||||
176,
|
|
||||||
16.196,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
61.725,
|
|
||||||
176,
|
|
||||||
7.8878,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.6128,
|
|
||||||
176,
|
|
||||||
28.4795,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
228,
|
|
||||||
13.8664,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
15.774,
|
|
||||||
228,
|
|
||||||
13.5589,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
29.333,
|
|
||||||
228,
|
|
||||||
16.196,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.529,
|
|
||||||
228,
|
|
||||||
16.196,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
61.725,
|
|
||||||
228,
|
|
||||||
7.8878,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.6128,
|
|
||||||
228,
|
|
||||||
28.4795,
|
|
||||||
52,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "4b74917f659334073252a738cfa9c4ac"
|
|
||||||
}
|
|
||||||
@@ -1,704 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "leave-requests",
|
|
||||||
"viewportWidth": 351,
|
|
||||||
"width": 351,
|
|
||||||
"height": 367,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
26,
|
|
||||||
100,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
53,
|
|
||||||
100,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
113,
|
|
||||||
100,
|
|
||||||
254,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
126,
|
|
||||||
21.1806,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.8843,
|
|
||||||
126,
|
|
||||||
20.6375,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.5217,
|
|
||||||
126,
|
|
||||||
25.3294,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.8511,
|
|
||||||
126,
|
|
||||||
25.3294,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
96.1806,
|
|
||||||
126,
|
|
||||||
10.3677,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
106.5483,
|
|
||||||
126,
|
|
||||||
20.7977,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
127.346,
|
|
||||||
126,
|
|
||||||
18.8079,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
157,
|
|
||||||
21.1806,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.8843,
|
|
||||||
157,
|
|
||||||
20.6375,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.5217,
|
|
||||||
157,
|
|
||||||
25.3294,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.8511,
|
|
||||||
157,
|
|
||||||
25.3294,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
96.1806,
|
|
||||||
157,
|
|
||||||
10.3677,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
106.5483,
|
|
||||||
157,
|
|
||||||
20.7977,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
127.346,
|
|
||||||
157,
|
|
||||||
18.8079,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
218,
|
|
||||||
21.1806,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.8843,
|
|
||||||
218,
|
|
||||||
20.6375,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.5217,
|
|
||||||
218,
|
|
||||||
25.3294,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.8511,
|
|
||||||
218,
|
|
||||||
25.3294,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
96.1806,
|
|
||||||
218,
|
|
||||||
10.3677,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
106.5483,
|
|
||||||
218,
|
|
||||||
20.7977,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
127.346,
|
|
||||||
218,
|
|
||||||
18.8079,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
279,
|
|
||||||
21.1806,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
24.8843,
|
|
||||||
279,
|
|
||||||
20.6375,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.5217,
|
|
||||||
279,
|
|
||||||
25.3294,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.8511,
|
|
||||||
279,
|
|
||||||
25.3294,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
96.1806,
|
|
||||||
279,
|
|
||||||
10.3677,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
106.5483,
|
|
||||||
279,
|
|
||||||
20.7977,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
127.346,
|
|
||||||
279,
|
|
||||||
18.8079,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "leave-requests",
|
|
||||||
"viewportWidth": 736,
|
|
||||||
"width": 736,
|
|
||||||
"height": 320,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
27.1888,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
27.1888,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
83.6914,
|
|
||||||
4,
|
|
||||||
16.3086,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
253,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
86,
|
|
||||||
14.313,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.8945,
|
|
||||||
86,
|
|
||||||
13.9585,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.853,
|
|
||||||
86,
|
|
||||||
17.0219,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.8749,
|
|
||||||
86,
|
|
||||||
17.0219,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.8968,
|
|
||||||
86,
|
|
||||||
7.3157,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.2126,
|
|
||||||
86,
|
|
||||||
13.2006,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4131,
|
|
||||||
86,
|
|
||||||
12.0053,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
119,
|
|
||||||
14.313,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.8945,
|
|
||||||
119,
|
|
||||||
13.9585,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.853,
|
|
||||||
119,
|
|
||||||
17.0219,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.8749,
|
|
||||||
119,
|
|
||||||
17.0219,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.8968,
|
|
||||||
119,
|
|
||||||
7.3157,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.2126,
|
|
||||||
119,
|
|
||||||
13.2006,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4131,
|
|
||||||
119,
|
|
||||||
12.0053,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
180,
|
|
||||||
14.313,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.8945,
|
|
||||||
180,
|
|
||||||
13.9585,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.853,
|
|
||||||
180,
|
|
||||||
17.0219,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.8749,
|
|
||||||
180,
|
|
||||||
17.0219,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.8968,
|
|
||||||
180,
|
|
||||||
7.3157,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.2126,
|
|
||||||
180,
|
|
||||||
13.2006,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4131,
|
|
||||||
180,
|
|
||||||
12.0053,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
241,
|
|
||||||
14.313,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.8945,
|
|
||||||
241,
|
|
||||||
13.9585,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.853,
|
|
||||||
241,
|
|
||||||
17.0219,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.8749,
|
|
||||||
241,
|
|
||||||
17.0219,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.8968,
|
|
||||||
241,
|
|
||||||
7.3157,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.2126,
|
|
||||||
241,
|
|
||||||
13.2006,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4131,
|
|
||||||
241,
|
|
||||||
12.0053,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "leave-requests",
|
|
||||||
"viewportWidth": 996,
|
|
||||||
"width": 996,
|
|
||||||
"height": 308,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
20.0913,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
20.0913,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
88.3503,
|
|
||||||
10,
|
|
||||||
11.6497,
|
|
||||||
32,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
241,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
86,
|
|
||||||
14.9787,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.8863,
|
|
||||||
86,
|
|
||||||
14.6461,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
31.5324,
|
|
||||||
86,
|
|
||||||
17.495,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.0274,
|
|
||||||
86,
|
|
||||||
17.495,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
66.5223,
|
|
||||||
86,
|
|
||||||
8.52,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
75.0424,
|
|
||||||
86,
|
|
||||||
12.74,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.7824,
|
|
||||||
86,
|
|
||||||
10.31,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
124,
|
|
||||||
14.9787,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.8863,
|
|
||||||
124,
|
|
||||||
14.6461,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
31.5324,
|
|
||||||
124,
|
|
||||||
17.495,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.0274,
|
|
||||||
124,
|
|
||||||
17.495,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
66.5223,
|
|
||||||
124,
|
|
||||||
8.52,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
75.0424,
|
|
||||||
124,
|
|
||||||
12.74,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.7824,
|
|
||||||
124,
|
|
||||||
10.31,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
179,
|
|
||||||
14.9787,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.8863,
|
|
||||||
179,
|
|
||||||
14.6461,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
31.5324,
|
|
||||||
179,
|
|
||||||
17.495,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.0274,
|
|
||||||
179,
|
|
||||||
17.495,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
66.5223,
|
|
||||||
179,
|
|
||||||
8.52,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
75.0424,
|
|
||||||
179,
|
|
||||||
12.74,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.7824,
|
|
||||||
179,
|
|
||||||
10.31,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
234,
|
|
||||||
14.9787,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
16.8863,
|
|
||||||
234,
|
|
||||||
14.6461,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
31.5324,
|
|
||||||
234,
|
|
||||||
17.495,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.0274,
|
|
||||||
234,
|
|
||||||
17.495,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
66.5223,
|
|
||||||
234,
|
|
||||||
8.52,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
75.0424,
|
|
||||||
234,
|
|
||||||
12.74,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.7824,
|
|
||||||
234,
|
|
||||||
10.31,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "125231cbf4c6abc4e73bb48732dc9353"
|
|
||||||
}
|
|
||||||
@@ -1,620 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "offer-detail",
|
|
||||||
"viewportWidth": 351,
|
|
||||||
"width": 351,
|
|
||||||
"height": 483,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
12.5356,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
52,
|
|
||||||
100,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
86,
|
|
||||||
100,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
146,
|
|
||||||
100,
|
|
||||||
338,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
159,
|
|
||||||
44.0171,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
187,
|
|
||||||
44.0171,
|
|
||||||
46,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.2792,
|
|
||||||
159,
|
|
||||||
44.0171,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.2792,
|
|
||||||
187,
|
|
||||||
44.0171,
|
|
||||||
46,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
249,
|
|
||||||
44.0171,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
278,
|
|
||||||
44.0171,
|
|
||||||
46,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.2792,
|
|
||||||
249,
|
|
||||||
44.0171,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.2792,
|
|
||||||
278,
|
|
||||||
44.0171,
|
|
||||||
46,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
339,
|
|
||||||
34.4062,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.1099,
|
|
||||||
339,
|
|
||||||
34.8157,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.9256,
|
|
||||||
339,
|
|
||||||
36.6097,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.5353,
|
|
||||||
339,
|
|
||||||
36.6186,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
370,
|
|
||||||
34.4062,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.1099,
|
|
||||||
370,
|
|
||||||
34.8157,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.9256,
|
|
||||||
370,
|
|
||||||
36.6097,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.5353,
|
|
||||||
370,
|
|
||||||
36.6186,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
404,
|
|
||||||
34.4062,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.1099,
|
|
||||||
404,
|
|
||||||
34.8157,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.9256,
|
|
||||||
404,
|
|
||||||
36.6097,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.5353,
|
|
||||||
404,
|
|
||||||
36.6186,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
437,
|
|
||||||
34.4062,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.1099,
|
|
||||||
437,
|
|
||||||
34.8157,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.9256,
|
|
||||||
437,
|
|
||||||
36.6097,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.5353,
|
|
||||||
437,
|
|
||||||
36.6186,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "offer-detail",
|
|
||||||
"viewportWidth": 736,
|
|
||||||
"width": 736,
|
|
||||||
"height": 416,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
5.9783,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.4829,
|
|
||||||
7,
|
|
||||||
19.8412,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
90.8267,
|
|
||||||
0,
|
|
||||||
9.1733,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
60,
|
|
||||||
100,
|
|
||||||
356,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
79,
|
|
||||||
46.3315,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
108,
|
|
||||||
46.3315,
|
|
||||||
46,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.087,
|
|
||||||
79,
|
|
||||||
46.3315,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.087,
|
|
||||||
108,
|
|
||||||
46.3315,
|
|
||||||
46,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
169,
|
|
||||||
46.3315,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
198,
|
|
||||||
46.3315,
|
|
||||||
46,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.087,
|
|
||||||
169,
|
|
||||||
46.3315,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.087,
|
|
||||||
198,
|
|
||||||
46.3315,
|
|
||||||
46,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
260,
|
|
||||||
22.8558,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.4373,
|
|
||||||
260,
|
|
||||||
23.4333,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.8706,
|
|
||||||
260,
|
|
||||||
24.2718,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.1424,
|
|
||||||
260,
|
|
||||||
24.2761,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
292,
|
|
||||||
22.8558,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.4373,
|
|
||||||
292,
|
|
||||||
23.4333,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.8706,
|
|
||||||
292,
|
|
||||||
24.2718,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.1424,
|
|
||||||
292,
|
|
||||||
24.2761,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
327,
|
|
||||||
22.8558,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.4373,
|
|
||||||
327,
|
|
||||||
23.4333,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.8706,
|
|
||||||
327,
|
|
||||||
24.2718,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.1424,
|
|
||||||
327,
|
|
||||||
24.2761,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
362,
|
|
||||||
22.8558,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.4373,
|
|
||||||
362,
|
|
||||||
23.4333,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.8706,
|
|
||||||
362,
|
|
||||||
24.2718,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.1424,
|
|
||||||
362,
|
|
||||||
24.2761,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "offer-detail",
|
|
||||||
"viewportWidth": 996,
|
|
||||||
"width": 996,
|
|
||||||
"height": 419,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
3.2129,
|
|
||||||
32,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.0878,
|
|
||||||
1,
|
|
||||||
14.6618,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
93.6229,
|
|
||||||
0,
|
|
||||||
6.3771,
|
|
||||||
32,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
48,
|
|
||||||
100,
|
|
||||||
371,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
67,
|
|
||||||
47.2892,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
94,
|
|
||||||
47.2892,
|
|
||||||
41,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
50.8032,
|
|
||||||
67,
|
|
||||||
47.2892,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
50.8032,
|
|
||||||
94,
|
|
||||||
47.2892,
|
|
||||||
41,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
151,
|
|
||||||
47.2892,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
178,
|
|
||||||
47.2892,
|
|
||||||
41,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
50.8032,
|
|
||||||
151,
|
|
||||||
47.2892,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
50.8032,
|
|
||||||
178,
|
|
||||||
47.2892,
|
|
||||||
41,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
235,
|
|
||||||
23.2194,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.1271,
|
|
||||||
235,
|
|
||||||
23.9787,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.1058,
|
|
||||||
235,
|
|
||||||
24.4917,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.5975,
|
|
||||||
235,
|
|
||||||
24.4949,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
273,
|
|
||||||
23.2194,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.1271,
|
|
||||||
273,
|
|
||||||
23.9787,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.1058,
|
|
||||||
273,
|
|
||||||
24.4917,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.5975,
|
|
||||||
273,
|
|
||||||
24.4949,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
316,
|
|
||||||
23.2194,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.1271,
|
|
||||||
316,
|
|
||||||
23.9787,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.1058,
|
|
||||||
316,
|
|
||||||
24.4917,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.5975,
|
|
||||||
316,
|
|
||||||
24.4949,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
358,
|
|
||||||
23.2194,
|
|
||||||
42,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.1271,
|
|
||||||
358,
|
|
||||||
23.9787,
|
|
||||||
42,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.1058,
|
|
||||||
358,
|
|
||||||
24.4917,
|
|
||||||
42,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
73.5975,
|
|
||||||
358,
|
|
||||||
24.4949,
|
|
||||||
42,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "67676fde7dd5c432922d819fc9bf48db"
|
|
||||||
}
|
|
||||||
@@ -1,641 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "offers-customers",
|
|
||||||
"viewportWidth": 351,
|
|
||||||
"width": 351,
|
|
||||||
"height": 549,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
26,
|
|
||||||
100,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
53,
|
|
||||||
100,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
113,
|
|
||||||
100,
|
|
||||||
436,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
126,
|
|
||||||
92.5926,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
186,
|
|
||||||
34.562,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.2657,
|
|
||||||
186,
|
|
||||||
23.7936,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.0593,
|
|
||||||
186,
|
|
||||||
32.4653,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
94.5246,
|
|
||||||
186,
|
|
||||||
51.6293,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
217,
|
|
||||||
34.562,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.2657,
|
|
||||||
217,
|
|
||||||
23.7936,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.0593,
|
|
||||||
217,
|
|
||||||
32.4653,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
94.5246,
|
|
||||||
217,
|
|
||||||
51.6293,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
278,
|
|
||||||
34.562,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.2657,
|
|
||||||
278,
|
|
||||||
23.7936,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.0593,
|
|
||||||
278,
|
|
||||||
32.4653,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
94.5246,
|
|
||||||
278,
|
|
||||||
51.6293,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
339,
|
|
||||||
34.562,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.2657,
|
|
||||||
339,
|
|
||||||
23.7936,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.0593,
|
|
||||||
339,
|
|
||||||
32.4653,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
94.5246,
|
|
||||||
339,
|
|
||||||
51.6293,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
400,
|
|
||||||
34.562,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.2657,
|
|
||||||
400,
|
|
||||||
23.7936,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.0593,
|
|
||||||
400,
|
|
||||||
32.4653,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
94.5246,
|
|
||||||
400,
|
|
||||||
51.6293,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
461,
|
|
||||||
34.562,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.2657,
|
|
||||||
461,
|
|
||||||
23.7936,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.0593,
|
|
||||||
461,
|
|
||||||
32.4653,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
94.5246,
|
|
||||||
461,
|
|
||||||
51.6293,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "offers-customers",
|
|
||||||
"viewportWidth": 736,
|
|
||||||
"width": 736,
|
|
||||||
"height": 502,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
12.9458,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
12.9458,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
80.6322,
|
|
||||||
4,
|
|
||||||
19.3678,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
435,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
86,
|
|
||||||
94.837,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
146,
|
|
||||||
23.2868,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.8683,
|
|
||||||
146,
|
|
||||||
16.453,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
42.3212,
|
|
||||||
146,
|
|
||||||
21.9175,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.2387,
|
|
||||||
146,
|
|
||||||
33.1798,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
179,
|
|
||||||
23.2868,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.8683,
|
|
||||||
179,
|
|
||||||
16.453,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
42.3212,
|
|
||||||
179,
|
|
||||||
21.9175,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.2387,
|
|
||||||
179,
|
|
||||||
33.1798,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
240,
|
|
||||||
23.2868,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.8683,
|
|
||||||
240,
|
|
||||||
16.453,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
42.3212,
|
|
||||||
240,
|
|
||||||
21.9175,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.2387,
|
|
||||||
240,
|
|
||||||
33.1798,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
301,
|
|
||||||
23.2868,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.8683,
|
|
||||||
301,
|
|
||||||
16.453,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
42.3212,
|
|
||||||
301,
|
|
||||||
21.9175,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.2387,
|
|
||||||
301,
|
|
||||||
33.1798,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
362,
|
|
||||||
23.2868,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.8683,
|
|
||||||
362,
|
|
||||||
16.453,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
42.3212,
|
|
||||||
362,
|
|
||||||
21.9175,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.2387,
|
|
||||||
362,
|
|
||||||
33.1798,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
423,
|
|
||||||
23.2868,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.8683,
|
|
||||||
423,
|
|
||||||
16.453,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
42.3212,
|
|
||||||
423,
|
|
||||||
21.9175,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
64.2387,
|
|
||||||
423,
|
|
||||||
33.1798,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "offers-customers",
|
|
||||||
"viewportWidth": 996,
|
|
||||||
"width": 996,
|
|
||||||
"height": 470,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
9.5664,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
9.5664,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
86.0897,
|
|
||||||
10,
|
|
||||||
13.9103,
|
|
||||||
32,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
403,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
86,
|
|
||||||
96.1847,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
138,
|
|
||||||
25.673,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.5806,
|
|
||||||
138,
|
|
||||||
19.0904,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
46.6711,
|
|
||||||
138,
|
|
||||||
24.3254,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.9965,
|
|
||||||
138,
|
|
||||||
27.0959,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
176,
|
|
||||||
25.673,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.5806,
|
|
||||||
176,
|
|
||||||
19.0904,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
46.6711,
|
|
||||||
176,
|
|
||||||
24.3254,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.9965,
|
|
||||||
176,
|
|
||||||
27.0959,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
231,
|
|
||||||
25.673,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.5806,
|
|
||||||
231,
|
|
||||||
19.0904,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
46.6711,
|
|
||||||
231,
|
|
||||||
24.3254,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.9965,
|
|
||||||
231,
|
|
||||||
27.0959,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
286,
|
|
||||||
25.673,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.5806,
|
|
||||||
286,
|
|
||||||
19.0904,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
46.6711,
|
|
||||||
286,
|
|
||||||
24.3254,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.9965,
|
|
||||||
286,
|
|
||||||
27.0959,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
341,
|
|
||||||
25.673,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.5806,
|
|
||||||
341,
|
|
||||||
19.0904,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
46.6711,
|
|
||||||
341,
|
|
||||||
24.3254,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.9965,
|
|
||||||
341,
|
|
||||||
27.0959,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
396,
|
|
||||||
25.673,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.5806,
|
|
||||||
396,
|
|
||||||
19.0904,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
46.6711,
|
|
||||||
396,
|
|
||||||
24.3254,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.9965,
|
|
||||||
396,
|
|
||||||
27.0959,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "63b2dec2b6ceb84d931a000ab8b669dd"
|
|
||||||
}
|
|
||||||
@@ -1,452 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "offers-templates",
|
|
||||||
"viewportWidth": 351,
|
|
||||||
"width": 351,
|
|
||||||
"height": 436,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
436,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
13,
|
|
||||||
92.5926,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
73,
|
|
||||||
39.659,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
43.3627,
|
|
||||||
73,
|
|
||||||
39.6857,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
83.0484,
|
|
||||||
73,
|
|
||||||
63.1054,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
104,
|
|
||||||
39.659,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
43.3627,
|
|
||||||
104,
|
|
||||||
39.6857,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
83.0484,
|
|
||||||
104,
|
|
||||||
63.1054,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
165,
|
|
||||||
39.659,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
43.3627,
|
|
||||||
165,
|
|
||||||
39.6857,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
83.0484,
|
|
||||||
165,
|
|
||||||
63.1054,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
226,
|
|
||||||
39.659,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
43.3627,
|
|
||||||
226,
|
|
||||||
39.6857,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
83.0484,
|
|
||||||
226,
|
|
||||||
63.1054,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
287,
|
|
||||||
39.659,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
43.3627,
|
|
||||||
287,
|
|
||||||
39.6857,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
83.0484,
|
|
||||||
287,
|
|
||||||
63.1054,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
348,
|
|
||||||
39.659,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
43.3627,
|
|
||||||
348,
|
|
||||||
39.6857,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
83.0484,
|
|
||||||
348,
|
|
||||||
63.1054,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "offers-templates",
|
|
||||||
"viewportWidth": 736,
|
|
||||||
"width": 736,
|
|
||||||
"height": 435,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
435,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
19,
|
|
||||||
94.837,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
79,
|
|
||||||
26.9744,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
29.5559,
|
|
||||||
79,
|
|
||||||
26.9977,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
56.5536,
|
|
||||||
79,
|
|
||||||
40.8649,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
112,
|
|
||||||
26.9744,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
29.5559,
|
|
||||||
112,
|
|
||||||
26.9977,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
56.5536,
|
|
||||||
112,
|
|
||||||
40.8649,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
173,
|
|
||||||
26.9744,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
29.5559,
|
|
||||||
173,
|
|
||||||
26.9977,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
56.5536,
|
|
||||||
173,
|
|
||||||
40.8649,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
234,
|
|
||||||
26.9744,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
29.5559,
|
|
||||||
234,
|
|
||||||
26.9977,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
56.5536,
|
|
||||||
234,
|
|
||||||
40.8649,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
295,
|
|
||||||
26.9744,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
29.5559,
|
|
||||||
295,
|
|
||||||
26.9977,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
56.5536,
|
|
||||||
295,
|
|
||||||
40.8649,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
356,
|
|
||||||
26.9744,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
29.5559,
|
|
||||||
356,
|
|
||||||
26.9977,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
56.5536,
|
|
||||||
356,
|
|
||||||
40.8649,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "offers-templates",
|
|
||||||
"viewportWidth": 996,
|
|
||||||
"width": 996,
|
|
||||||
"height": 403,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
403,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
19,
|
|
||||||
96.1847,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
71,
|
|
||||||
30.8719,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
32.7796,
|
|
||||||
71,
|
|
||||||
30.897,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
63.6766,
|
|
||||||
71,
|
|
||||||
34.4158,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
109,
|
|
||||||
30.8719,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
32.7796,
|
|
||||||
109,
|
|
||||||
30.897,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
63.6766,
|
|
||||||
109,
|
|
||||||
34.4158,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
164,
|
|
||||||
30.8719,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
32.7796,
|
|
||||||
164,
|
|
||||||
30.897,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
63.6766,
|
|
||||||
164,
|
|
||||||
34.4158,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
219,
|
|
||||||
30.8719,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
32.7796,
|
|
||||||
219,
|
|
||||||
30.897,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
63.6766,
|
|
||||||
219,
|
|
||||||
34.4158,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
274,
|
|
||||||
30.8719,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
32.7796,
|
|
||||||
274,
|
|
||||||
30.897,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
63.6766,
|
|
||||||
274,
|
|
||||||
34.4158,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
329,
|
|
||||||
30.8719,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
32.7796,
|
|
||||||
329,
|
|
||||||
30.897,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
63.6766,
|
|
||||||
329,
|
|
||||||
34.4158,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "5e5881859bd932a42345c69a6a30ca65"
|
|
||||||
}
|
|
||||||
@@ -1,872 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "offers",
|
|
||||||
"viewportWidth": 351,
|
|
||||||
"width": 351,
|
|
||||||
"height": 497,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
26,
|
|
||||||
100,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
61,
|
|
||||||
100,
|
|
||||||
436,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
74,
|
|
||||||
92.5926,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
134,
|
|
||||||
26.7495,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.4532,
|
|
||||||
134,
|
|
||||||
20.6019,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.055,
|
|
||||||
134,
|
|
||||||
21.4165,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.4715,
|
|
||||||
134,
|
|
||||||
23.0502,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
95.5217,
|
|
||||||
134,
|
|
||||||
23.0502,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
118.5719,
|
|
||||||
134,
|
|
||||||
30.7692,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
165,
|
|
||||||
26.7495,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.4532,
|
|
||||||
165,
|
|
||||||
20.6019,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.055,
|
|
||||||
165,
|
|
||||||
21.4165,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.4715,
|
|
||||||
165,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
95.5217,
|
|
||||||
165,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
118.5719,
|
|
||||||
165,
|
|
||||||
30.7692,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
226,
|
|
||||||
26.7495,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.4532,
|
|
||||||
226,
|
|
||||||
20.6019,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.055,
|
|
||||||
226,
|
|
||||||
21.4165,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.4715,
|
|
||||||
226,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
95.5217,
|
|
||||||
226,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
118.5719,
|
|
||||||
226,
|
|
||||||
30.7692,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
287,
|
|
||||||
26.7495,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.4532,
|
|
||||||
287,
|
|
||||||
20.6019,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.055,
|
|
||||||
287,
|
|
||||||
21.4165,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.4715,
|
|
||||||
287,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
95.5217,
|
|
||||||
287,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
118.5719,
|
|
||||||
287,
|
|
||||||
30.7692,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
348,
|
|
||||||
26.7495,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.4532,
|
|
||||||
348,
|
|
||||||
20.6019,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.055,
|
|
||||||
348,
|
|
||||||
21.4165,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.4715,
|
|
||||||
348,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
95.5217,
|
|
||||||
348,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
118.5719,
|
|
||||||
348,
|
|
||||||
30.7692,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
409,
|
|
||||||
26.7495,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.4532,
|
|
||||||
409,
|
|
||||||
20.6019,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.055,
|
|
||||||
409,
|
|
||||||
21.4165,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.4715,
|
|
||||||
409,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
95.5217,
|
|
||||||
409,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
118.5719,
|
|
||||||
409,
|
|
||||||
30.7692,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "offers",
|
|
||||||
"viewportWidth": 736,
|
|
||||||
"width": 736,
|
|
||||||
"height": 502,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
11.3451,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
11.3451,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
435,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
86,
|
|
||||||
94.837,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
146,
|
|
||||||
17.6779,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.2594,
|
|
||||||
146,
|
|
||||||
13.7101,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
33.9695,
|
|
||||||
146,
|
|
||||||
13.3301,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.2996,
|
|
||||||
146,
|
|
||||||
15.2917,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.5913,
|
|
||||||
146,
|
|
||||||
15.2917,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
77.883,
|
|
||||||
146,
|
|
||||||
19.5355,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
179,
|
|
||||||
17.6779,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.2594,
|
|
||||||
179,
|
|
||||||
13.7101,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
33.9695,
|
|
||||||
179,
|
|
||||||
13.3301,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.2996,
|
|
||||||
179,
|
|
||||||
15.2917,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.5913,
|
|
||||||
179,
|
|
||||||
15.2917,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
77.883,
|
|
||||||
179,
|
|
||||||
19.5355,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
240,
|
|
||||||
17.6779,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.2594,
|
|
||||||
240,
|
|
||||||
13.7101,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
33.9695,
|
|
||||||
240,
|
|
||||||
13.3301,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.2996,
|
|
||||||
240,
|
|
||||||
15.2917,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.5913,
|
|
||||||
240,
|
|
||||||
15.2917,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
77.883,
|
|
||||||
240,
|
|
||||||
19.5355,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
301,
|
|
||||||
17.6779,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.2594,
|
|
||||||
301,
|
|
||||||
13.7101,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
33.9695,
|
|
||||||
301,
|
|
||||||
13.3301,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.2996,
|
|
||||||
301,
|
|
||||||
15.2917,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.5913,
|
|
||||||
301,
|
|
||||||
15.2917,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
77.883,
|
|
||||||
301,
|
|
||||||
19.5355,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
362,
|
|
||||||
17.6779,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.2594,
|
|
||||||
362,
|
|
||||||
13.7101,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
33.9695,
|
|
||||||
362,
|
|
||||||
13.3301,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.2996,
|
|
||||||
362,
|
|
||||||
15.2917,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.5913,
|
|
||||||
362,
|
|
||||||
15.2917,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
77.883,
|
|
||||||
362,
|
|
||||||
19.5355,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
423,
|
|
||||||
17.6779,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.2594,
|
|
||||||
423,
|
|
||||||
13.7101,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
33.9695,
|
|
||||||
423,
|
|
||||||
13.3301,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.2996,
|
|
||||||
423,
|
|
||||||
15.2917,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.5913,
|
|
||||||
423,
|
|
||||||
15.2917,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
77.883,
|
|
||||||
423,
|
|
||||||
19.5355,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "offers",
|
|
||||||
"viewportWidth": 996,
|
|
||||||
"width": 996,
|
|
||||||
"height": 470,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
8.3835,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
8.3835,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
403,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
86,
|
|
||||||
96.1847,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
138,
|
|
||||||
18.8912,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.7988,
|
|
||||||
138,
|
|
||||||
15.0085,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.8073,
|
|
||||||
138,
|
|
||||||
13.333,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.1403,
|
|
||||||
138,
|
|
||||||
16.5553,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.6956,
|
|
||||||
138,
|
|
||||||
16.5553,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
82.2509,
|
|
||||||
138,
|
|
||||||
15.8415,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
176,
|
|
||||||
18.8912,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.7988,
|
|
||||||
176,
|
|
||||||
15.0085,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.8073,
|
|
||||||
176,
|
|
||||||
13.333,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.1403,
|
|
||||||
176,
|
|
||||||
16.5553,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.6956,
|
|
||||||
176,
|
|
||||||
16.5553,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
82.2509,
|
|
||||||
176,
|
|
||||||
15.8415,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
231,
|
|
||||||
18.8912,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.7988,
|
|
||||||
231,
|
|
||||||
15.0085,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.8073,
|
|
||||||
231,
|
|
||||||
13.333,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.1403,
|
|
||||||
231,
|
|
||||||
16.5553,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.6956,
|
|
||||||
231,
|
|
||||||
16.5553,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
82.2509,
|
|
||||||
231,
|
|
||||||
15.8415,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
286,
|
|
||||||
18.8912,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.7988,
|
|
||||||
286,
|
|
||||||
15.0085,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.8073,
|
|
||||||
286,
|
|
||||||
13.333,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.1403,
|
|
||||||
286,
|
|
||||||
16.5553,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.6956,
|
|
||||||
286,
|
|
||||||
16.5553,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
82.2509,
|
|
||||||
286,
|
|
||||||
15.8415,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
341,
|
|
||||||
18.8912,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.7988,
|
|
||||||
341,
|
|
||||||
15.0085,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.8073,
|
|
||||||
341,
|
|
||||||
13.333,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.1403,
|
|
||||||
341,
|
|
||||||
16.5553,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.6956,
|
|
||||||
341,
|
|
||||||
16.5553,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
82.2509,
|
|
||||||
341,
|
|
||||||
15.8415,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
396,
|
|
||||||
18.8912,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.7988,
|
|
||||||
396,
|
|
||||||
15.0085,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.8073,
|
|
||||||
396,
|
|
||||||
13.333,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
49.1403,
|
|
||||||
396,
|
|
||||||
16.5553,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.6956,
|
|
||||||
396,
|
|
||||||
16.5553,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
82.2509,
|
|
||||||
396,
|
|
||||||
15.8415,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "62d793eb0343d832087d687b76639e09"
|
|
||||||
}
|
|
||||||
@@ -1,998 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "orders",
|
|
||||||
"viewportWidth": 351,
|
|
||||||
"width": 351,
|
|
||||||
"height": 497,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
26,
|
|
||||||
100,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
61,
|
|
||||||
100,
|
|
||||||
436,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
74,
|
|
||||||
92.5926,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
134,
|
|
||||||
26.7495,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.4532,
|
|
||||||
134,
|
|
||||||
29.1043,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
59.5575,
|
|
||||||
134,
|
|
||||||
20.6019,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
80.1594,
|
|
||||||
134,
|
|
||||||
26.6515,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
106.8109,
|
|
||||||
134,
|
|
||||||
23.0502,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
129.8611,
|
|
||||||
134,
|
|
||||||
21.2028,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
151.0639,
|
|
||||||
134,
|
|
||||||
17.094,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
165,
|
|
||||||
26.7495,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.4532,
|
|
||||||
165,
|
|
||||||
29.1043,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
59.5575,
|
|
||||||
165,
|
|
||||||
20.6019,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
80.1594,
|
|
||||||
165,
|
|
||||||
26.6515,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
106.8109,
|
|
||||||
165,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
129.8611,
|
|
||||||
165,
|
|
||||||
21.2028,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
151.0639,
|
|
||||||
165,
|
|
||||||
17.094,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
226,
|
|
||||||
26.7495,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.4532,
|
|
||||||
226,
|
|
||||||
29.1043,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
59.5575,
|
|
||||||
226,
|
|
||||||
20.6019,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
80.1594,
|
|
||||||
226,
|
|
||||||
26.6515,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
106.8109,
|
|
||||||
226,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
129.8611,
|
|
||||||
226,
|
|
||||||
21.2028,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
151.0639,
|
|
||||||
226,
|
|
||||||
17.094,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
287,
|
|
||||||
26.7495,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.4532,
|
|
||||||
287,
|
|
||||||
29.1043,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
59.5575,
|
|
||||||
287,
|
|
||||||
20.6019,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
80.1594,
|
|
||||||
287,
|
|
||||||
26.6515,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
106.8109,
|
|
||||||
287,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
129.8611,
|
|
||||||
287,
|
|
||||||
21.2028,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
151.0639,
|
|
||||||
287,
|
|
||||||
17.094,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
348,
|
|
||||||
26.7495,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.4532,
|
|
||||||
348,
|
|
||||||
29.1043,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
59.5575,
|
|
||||||
348,
|
|
||||||
20.6019,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
80.1594,
|
|
||||||
348,
|
|
||||||
26.6515,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
106.8109,
|
|
||||||
348,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
129.8611,
|
|
||||||
348,
|
|
||||||
21.2028,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
151.0639,
|
|
||||||
348,
|
|
||||||
17.094,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
409,
|
|
||||||
26.7495,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
30.4532,
|
|
||||||
409,
|
|
||||||
29.1043,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
59.5575,
|
|
||||||
409,
|
|
||||||
20.6019,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
80.1594,
|
|
||||||
409,
|
|
||||||
26.6515,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
106.8109,
|
|
||||||
409,
|
|
||||||
23.0502,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
129.8611,
|
|
||||||
409,
|
|
||||||
21.2028,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
151.0639,
|
|
||||||
409,
|
|
||||||
17.094,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "orders",
|
|
||||||
"viewportWidth": 736,
|
|
||||||
"width": 736,
|
|
||||||
"height": 502,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
16.6461,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
16.6461,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
435,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
86,
|
|
||||||
94.837,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
146,
|
|
||||||
15.642,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.2235,
|
|
||||||
146,
|
|
||||||
16.9837,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.2072,
|
|
||||||
146,
|
|
||||||
12.1306,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.3378,
|
|
||||||
146,
|
|
||||||
14.5338,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
61.8716,
|
|
||||||
146,
|
|
||||||
13.5296,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
75.4012,
|
|
||||||
146,
|
|
||||||
12.4745,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.8758,
|
|
||||||
146,
|
|
||||||
9.5427,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
179,
|
|
||||||
15.642,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.2235,
|
|
||||||
179,
|
|
||||||
16.9837,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.2072,
|
|
||||||
179,
|
|
||||||
12.1306,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.3378,
|
|
||||||
179,
|
|
||||||
14.5338,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
61.8716,
|
|
||||||
179,
|
|
||||||
13.5296,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
75.4012,
|
|
||||||
179,
|
|
||||||
12.4745,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.8758,
|
|
||||||
179,
|
|
||||||
9.5427,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
240,
|
|
||||||
15.642,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.2235,
|
|
||||||
240,
|
|
||||||
16.9837,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.2072,
|
|
||||||
240,
|
|
||||||
12.1306,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.3378,
|
|
||||||
240,
|
|
||||||
14.5338,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
61.8716,
|
|
||||||
240,
|
|
||||||
13.5296,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
75.4012,
|
|
||||||
240,
|
|
||||||
12.4745,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.8758,
|
|
||||||
240,
|
|
||||||
9.5427,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
301,
|
|
||||||
15.642,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.2235,
|
|
||||||
301,
|
|
||||||
16.9837,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.2072,
|
|
||||||
301,
|
|
||||||
12.1306,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.3378,
|
|
||||||
301,
|
|
||||||
14.5338,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
61.8716,
|
|
||||||
301,
|
|
||||||
13.5296,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
75.4012,
|
|
||||||
301,
|
|
||||||
12.4745,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.8758,
|
|
||||||
301,
|
|
||||||
9.5427,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
362,
|
|
||||||
15.642,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.2235,
|
|
||||||
362,
|
|
||||||
16.9837,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.2072,
|
|
||||||
362,
|
|
||||||
12.1306,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.3378,
|
|
||||||
362,
|
|
||||||
14.5338,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
61.8716,
|
|
||||||
362,
|
|
||||||
13.5296,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
75.4012,
|
|
||||||
362,
|
|
||||||
12.4745,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.8758,
|
|
||||||
362,
|
|
||||||
9.5427,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
423,
|
|
||||||
15.642,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.2235,
|
|
||||||
423,
|
|
||||||
16.9837,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.2072,
|
|
||||||
423,
|
|
||||||
12.1306,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.3378,
|
|
||||||
423,
|
|
||||||
14.5338,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
61.8716,
|
|
||||||
423,
|
|
||||||
13.5296,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
75.4012,
|
|
||||||
423,
|
|
||||||
12.4745,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.8758,
|
|
||||||
423,
|
|
||||||
9.5427,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "orders",
|
|
||||||
"viewportWidth": 996,
|
|
||||||
"width": 996,
|
|
||||||
"height": 470,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
12.3008,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
12.3008,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
403,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
86,
|
|
||||||
96.1847,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
138,
|
|
||||||
16.2243,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.1319,
|
|
||||||
138,
|
|
||||||
17.5044,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.6363,
|
|
||||||
138,
|
|
||||||
12.8891,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.5254,
|
|
||||||
138,
|
|
||||||
13.7535,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.2788,
|
|
||||||
138,
|
|
||||||
14.2178,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
76.4966,
|
|
||||||
138,
|
|
||||||
13.2138,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
89.7104,
|
|
||||||
138,
|
|
||||||
8.382,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
176,
|
|
||||||
16.2243,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.1319,
|
|
||||||
176,
|
|
||||||
17.5044,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.6363,
|
|
||||||
176,
|
|
||||||
12.8891,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.5254,
|
|
||||||
176,
|
|
||||||
13.7535,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.2788,
|
|
||||||
176,
|
|
||||||
14.2178,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
76.4966,
|
|
||||||
176,
|
|
||||||
13.2138,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
89.7104,
|
|
||||||
176,
|
|
||||||
8.382,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
231,
|
|
||||||
16.2243,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.1319,
|
|
||||||
231,
|
|
||||||
17.5044,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.6363,
|
|
||||||
231,
|
|
||||||
12.8891,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.5254,
|
|
||||||
231,
|
|
||||||
13.7535,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.2788,
|
|
||||||
231,
|
|
||||||
14.2178,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
76.4966,
|
|
||||||
231,
|
|
||||||
13.2138,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
89.7104,
|
|
||||||
231,
|
|
||||||
8.382,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
286,
|
|
||||||
16.2243,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.1319,
|
|
||||||
286,
|
|
||||||
17.5044,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.6363,
|
|
||||||
286,
|
|
||||||
12.8891,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.5254,
|
|
||||||
286,
|
|
||||||
13.7535,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.2788,
|
|
||||||
286,
|
|
||||||
14.2178,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
76.4966,
|
|
||||||
286,
|
|
||||||
13.2138,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
89.7104,
|
|
||||||
286,
|
|
||||||
8.382,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
341,
|
|
||||||
16.2243,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.1319,
|
|
||||||
341,
|
|
||||||
17.5044,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.6363,
|
|
||||||
341,
|
|
||||||
12.8891,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.5254,
|
|
||||||
341,
|
|
||||||
13.7535,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.2788,
|
|
||||||
341,
|
|
||||||
14.2178,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
76.4966,
|
|
||||||
341,
|
|
||||||
13.2138,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
89.7104,
|
|
||||||
341,
|
|
||||||
8.382,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
396,
|
|
||||||
16.2243,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.1319,
|
|
||||||
396,
|
|
||||||
17.5044,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.6363,
|
|
||||||
396,
|
|
||||||
12.8891,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.5254,
|
|
||||||
396,
|
|
||||||
13.7535,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
62.2788,
|
|
||||||
396,
|
|
||||||
14.2178,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
76.4966,
|
|
||||||
396,
|
|
||||||
13.2138,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
89.7104,
|
|
||||||
396,
|
|
||||||
8.382,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "677a0002aa805c9f7790bc68c6374bb5"
|
|
||||||
}
|
|
||||||
@@ -1,371 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "project-detail",
|
|
||||||
"viewportWidth": 351,
|
|
||||||
"width": 351,
|
|
||||||
"height": 481,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
3.4188,
|
|
||||||
12,
|
|
||||||
5.698,
|
|
||||||
20,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
14.8148,
|
|
||||||
9,
|
|
||||||
50.2315,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
52,
|
|
||||||
48.0057,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.4245,
|
|
||||||
52,
|
|
||||||
48.5755,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
112,
|
|
||||||
100,
|
|
||||||
188,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
125,
|
|
||||||
48.1481,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
154,
|
|
||||||
48.1481,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
56.4103,
|
|
||||||
125,
|
|
||||||
48.1481,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
56.4103,
|
|
||||||
154,
|
|
||||||
48.1481,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
214,
|
|
||||||
48.1481,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
243,
|
|
||||||
48.1481,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
56.4103,
|
|
||||||
214,
|
|
||||||
48.1481,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
56.4103,
|
|
||||||
243,
|
|
||||||
48.1481,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
316,
|
|
||||||
100,
|
|
||||||
165,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
329,
|
|
||||||
92.5926,
|
|
||||||
17,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
357,
|
|
||||||
92.5926,
|
|
||||||
104,
|
|
||||||
8
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "project-detail",
|
|
||||||
"viewportWidth": 736,
|
|
||||||
"width": 736,
|
|
||||||
"height": 453,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
1.6304,
|
|
||||||
12,
|
|
||||||
2.7174,
|
|
||||||
20,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
7.0652,
|
|
||||||
7,
|
|
||||||
29.2799,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
78.2375,
|
|
||||||
0,
|
|
||||||
9.1733,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
89.0413,
|
|
||||||
0,
|
|
||||||
10.9587,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
60,
|
|
||||||
100,
|
|
||||||
200,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
79,
|
|
||||||
46.3315,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
108,
|
|
||||||
46.3315,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.087,
|
|
||||||
79,
|
|
||||||
46.3315,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.087,
|
|
||||||
108,
|
|
||||||
46.3315,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
168,
|
|
||||||
46.3315,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
197,
|
|
||||||
46.3315,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.087,
|
|
||||||
168,
|
|
||||||
46.3315,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.087,
|
|
||||||
197,
|
|
||||||
46.3315,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
276,
|
|
||||||
100,
|
|
||||||
177,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
295,
|
|
||||||
94.837,
|
|
||||||
17,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
323,
|
|
||||||
94.837,
|
|
||||||
104,
|
|
||||||
8
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "project-detail",
|
|
||||||
"viewportWidth": 996,
|
|
||||||
"width": 996,
|
|
||||||
"height": 404,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0.6024,
|
|
||||||
7,
|
|
||||||
2.008,
|
|
||||||
20,
|
|
||||||
"50%"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.0161,
|
|
||||||
2,
|
|
||||||
21.6365,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
84.7217,
|
|
||||||
0,
|
|
||||||
6.3771,
|
|
||||||
34,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
92.3036,
|
|
||||||
0,
|
|
||||||
7.6964,
|
|
||||||
34,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
50,
|
|
||||||
100,
|
|
||||||
180,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
69,
|
|
||||||
47.2892,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
96,
|
|
||||||
47.2892,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
50.8032,
|
|
||||||
69,
|
|
||||||
47.2892,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
50.8032,
|
|
||||||
96,
|
|
||||||
47.2892,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
148,
|
|
||||||
47.2892,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
175,
|
|
||||||
47.2892,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
50.8032,
|
|
||||||
148,
|
|
||||||
47.2892,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
50.8032,
|
|
||||||
175,
|
|
||||||
47.2892,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
246,
|
|
||||||
100,
|
|
||||||
157,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
265,
|
|
||||||
96.1847,
|
|
||||||
17,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
294,
|
|
||||||
96.1847,
|
|
||||||
84,
|
|
||||||
8
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "ab5e1f108d42c55b0e6382fcaffff793"
|
|
||||||
}
|
|
||||||
@@ -1,746 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "projects",
|
|
||||||
"viewportWidth": 351,
|
|
||||||
"width": 351,
|
|
||||||
"height": 497,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
26,
|
|
||||||
100,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
61,
|
|
||||||
100,
|
|
||||||
436,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
74,
|
|
||||||
92.5926,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
134,
|
|
||||||
34.6065,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.3102,
|
|
||||||
134,
|
|
||||||
31.3568,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.667,
|
|
||||||
134,
|
|
||||||
26.6515,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
96.3186,
|
|
||||||
134,
|
|
||||||
27.7066,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
124.0251,
|
|
||||||
134,
|
|
||||||
22.1287,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
165,
|
|
||||||
34.6065,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.3102,
|
|
||||||
165,
|
|
||||||
31.3568,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.667,
|
|
||||||
165,
|
|
||||||
26.6515,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
96.3186,
|
|
||||||
165,
|
|
||||||
27.7066,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
124.0251,
|
|
||||||
165,
|
|
||||||
22.1287,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
226,
|
|
||||||
34.6065,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.3102,
|
|
||||||
226,
|
|
||||||
31.3568,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.667,
|
|
||||||
226,
|
|
||||||
26.6515,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
96.3186,
|
|
||||||
226,
|
|
||||||
27.7066,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
124.0251,
|
|
||||||
226,
|
|
||||||
22.1287,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
287,
|
|
||||||
34.6065,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.3102,
|
|
||||||
287,
|
|
||||||
31.3568,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.667,
|
|
||||||
287,
|
|
||||||
26.6515,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
96.3186,
|
|
||||||
287,
|
|
||||||
27.7066,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
124.0251,
|
|
||||||
287,
|
|
||||||
22.1287,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
348,
|
|
||||||
34.6065,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.3102,
|
|
||||||
348,
|
|
||||||
31.3568,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.667,
|
|
||||||
348,
|
|
||||||
26.6515,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
96.3186,
|
|
||||||
348,
|
|
||||||
27.7066,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
124.0251,
|
|
||||||
348,
|
|
||||||
22.1287,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
409,
|
|
||||||
34.6065,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.3102,
|
|
||||||
409,
|
|
||||||
31.3568,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.667,
|
|
||||||
409,
|
|
||||||
26.6515,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
96.3186,
|
|
||||||
409,
|
|
||||||
27.7066,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
124.0251,
|
|
||||||
409,
|
|
||||||
22.1287,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "projects",
|
|
||||||
"viewportWidth": 736,
|
|
||||||
"width": 736,
|
|
||||||
"height": 502,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
10.9099,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
10.9099,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
435,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
86,
|
|
||||||
94.837,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
146,
|
|
||||||
23.429,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.0105,
|
|
||||||
146,
|
|
||||||
21.2806,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.2911,
|
|
||||||
146,
|
|
||||||
18.1704,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.4615,
|
|
||||||
146,
|
|
||||||
17.6694,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
83.1309,
|
|
||||||
146,
|
|
||||||
14.2875,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
179,
|
|
||||||
23.429,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.0105,
|
|
||||||
179,
|
|
||||||
21.2806,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.2911,
|
|
||||||
179,
|
|
||||||
18.1704,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.4615,
|
|
||||||
179,
|
|
||||||
17.6694,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
83.1309,
|
|
||||||
179,
|
|
||||||
14.2875,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
240,
|
|
||||||
23.429,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.0105,
|
|
||||||
240,
|
|
||||||
21.2806,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.2911,
|
|
||||||
240,
|
|
||||||
18.1704,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.4615,
|
|
||||||
240,
|
|
||||||
17.6694,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
83.1309,
|
|
||||||
240,
|
|
||||||
14.2875,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
301,
|
|
||||||
23.429,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.0105,
|
|
||||||
301,
|
|
||||||
21.2806,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.2911,
|
|
||||||
301,
|
|
||||||
18.1704,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.4615,
|
|
||||||
301,
|
|
||||||
17.6694,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
83.1309,
|
|
||||||
301,
|
|
||||||
14.2875,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
362,
|
|
||||||
23.429,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.0105,
|
|
||||||
362,
|
|
||||||
21.2806,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.2911,
|
|
||||||
362,
|
|
||||||
18.1704,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.4615,
|
|
||||||
362,
|
|
||||||
17.6694,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
83.1309,
|
|
||||||
362,
|
|
||||||
14.2875,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
423,
|
|
||||||
23.429,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.0105,
|
|
||||||
423,
|
|
||||||
21.2806,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.2911,
|
|
||||||
423,
|
|
||||||
18.1704,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.4615,
|
|
||||||
423,
|
|
||||||
17.6694,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
83.1309,
|
|
||||||
423,
|
|
||||||
14.2875,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "projects",
|
|
||||||
"viewportWidth": 996,
|
|
||||||
"width": 996,
|
|
||||||
"height": 470,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
8.0619,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
8.0619,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
403,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
86,
|
|
||||||
96.1847,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
138,
|
|
||||||
24.4588,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.3664,
|
|
||||||
138,
|
|
||||||
22.4068,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.7732,
|
|
||||||
138,
|
|
||||||
19.4308,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
68.2041,
|
|
||||||
138,
|
|
||||||
17.2612,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4653,
|
|
||||||
138,
|
|
||||||
12.6271,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
176,
|
|
||||||
24.4588,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.3664,
|
|
||||||
176,
|
|
||||||
22.4068,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.7732,
|
|
||||||
176,
|
|
||||||
19.4308,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
68.2041,
|
|
||||||
176,
|
|
||||||
17.2612,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4653,
|
|
||||||
176,
|
|
||||||
12.6271,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
231,
|
|
||||||
24.4588,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.3664,
|
|
||||||
231,
|
|
||||||
22.4068,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.7732,
|
|
||||||
231,
|
|
||||||
19.4308,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
68.2041,
|
|
||||||
231,
|
|
||||||
17.2612,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4653,
|
|
||||||
231,
|
|
||||||
12.6271,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
286,
|
|
||||||
24.4588,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.3664,
|
|
||||||
286,
|
|
||||||
22.4068,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.7732,
|
|
||||||
286,
|
|
||||||
19.4308,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
68.2041,
|
|
||||||
286,
|
|
||||||
17.2612,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4653,
|
|
||||||
286,
|
|
||||||
12.6271,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
341,
|
|
||||||
24.4588,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.3664,
|
|
||||||
341,
|
|
||||||
22.4068,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.7732,
|
|
||||||
341,
|
|
||||||
19.4308,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
68.2041,
|
|
||||||
341,
|
|
||||||
17.2612,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4653,
|
|
||||||
341,
|
|
||||||
12.6271,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
396,
|
|
||||||
24.4588,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.3664,
|
|
||||||
396,
|
|
||||||
22.4068,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
48.7732,
|
|
||||||
396,
|
|
||||||
19.4308,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
68.2041,
|
|
||||||
396,
|
|
||||||
17.2612,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4653,
|
|
||||||
396,
|
|
||||||
12.6271,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "17f8285c3ca514ddef6d48c1183ed642"
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
"use client"
|
|
||||||
// Auto-generated by `npx boneyard-js build` — do not edit
|
|
||||||
import { registerBones } from 'boneyard-js'
|
|
||||||
import { configureBoneyard } from 'boneyard-js/react'
|
|
||||||
|
|
||||||
import _dash_sessions from './dash-sessions.bones.json'
|
|
||||||
import _attendance_history_fund from './attendance-history-fund.bones.json'
|
|
||||||
import _attendance_history_table from './attendance-history-table.bones.json'
|
|
||||||
import _leave_requests from './leave-requests.bones.json'
|
|
||||||
import _leave_approval from './leave-approval.bones.json'
|
|
||||||
import _attendance_balances from './attendance-balances.bones.json'
|
|
||||||
import _trips_history from './trips-history.bones.json'
|
|
||||||
import _trips_admin from './trips-admin.bones.json'
|
|
||||||
import _vehicles from './vehicles.bones.json'
|
|
||||||
import _offers from './offers.bones.json'
|
|
||||||
import _orders from './orders.bones.json'
|
|
||||||
import _projects from './projects.bones.json'
|
|
||||||
import _offers_customers from './offers-customers.bones.json'
|
|
||||||
import _users from './users.bones.json'
|
|
||||||
import _audit_log_rows from './audit-log-rows.bones.json'
|
|
||||||
import _offer_detail from './offer-detail.bones.json'
|
|
||||||
import _invoice_detail from './invoice-detail.bones.json'
|
|
||||||
import _project_detail from './project-detail.bones.json'
|
|
||||||
import _attendance_create from './attendance-create.bones.json'
|
|
||||||
import _offers_templates from './offers-templates.bones.json'
|
|
||||||
|
|
||||||
configureBoneyard({"color":"#e0e0e0","animate":"shimmer","shimmerColor":"#f0f0f0","speed":"1.2s","shimmerAngle":110})
|
|
||||||
|
|
||||||
registerBones({
|
|
||||||
"dash-sessions": _dash_sessions,
|
|
||||||
"attendance-history-fund": _attendance_history_fund,
|
|
||||||
"attendance-history-table": _attendance_history_table,
|
|
||||||
"leave-requests": _leave_requests,
|
|
||||||
"leave-approval": _leave_approval,
|
|
||||||
"attendance-balances": _attendance_balances,
|
|
||||||
"trips-history": _trips_history,
|
|
||||||
"trips-admin": _trips_admin,
|
|
||||||
"vehicles": _vehicles,
|
|
||||||
"offers": _offers,
|
|
||||||
"orders": _orders,
|
|
||||||
"projects": _projects,
|
|
||||||
"offers-customers": _offers_customers,
|
|
||||||
"users": _users,
|
|
||||||
"audit-log-rows": _audit_log_rows,
|
|
||||||
"offer-detail": _offer_detail,
|
|
||||||
"invoice-detail": _invoice_detail,
|
|
||||||
"project-detail": _project_detail,
|
|
||||||
"attendance-create": _attendance_create,
|
|
||||||
"offers-templates": _offers_templates,
|
|
||||||
})
|
|
||||||
@@ -1,725 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "trips-admin",
|
|
||||||
"viewportWidth": 317,
|
|
||||||
"width": 317,
|
|
||||||
"height": 437,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
26,
|
|
||||||
100,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
61,
|
|
||||||
100,
|
|
||||||
376,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.1009,
|
|
||||||
74,
|
|
||||||
37.8795,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.9805,
|
|
||||||
74,
|
|
||||||
43.4592,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4397,
|
|
||||||
74,
|
|
||||||
31.6739,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
117.1136,
|
|
||||||
74,
|
|
||||||
16.6108,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
133.7244,
|
|
||||||
74,
|
|
||||||
28.1053,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.1009,
|
|
||||||
105,
|
|
||||||
37.8795,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.9805,
|
|
||||||
105,
|
|
||||||
43.4592,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4397,
|
|
||||||
105,
|
|
||||||
31.6739,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
117.1136,
|
|
||||||
105,
|
|
||||||
16.6108,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
133.7244,
|
|
||||||
105,
|
|
||||||
28.1053,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.1009,
|
|
||||||
166,
|
|
||||||
37.8795,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.9805,
|
|
||||||
166,
|
|
||||||
43.4592,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4397,
|
|
||||||
166,
|
|
||||||
31.6739,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
117.1136,
|
|
||||||
166,
|
|
||||||
16.6108,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
133.7244,
|
|
||||||
166,
|
|
||||||
28.1053,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.1009,
|
|
||||||
227,
|
|
||||||
37.8795,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.9805,
|
|
||||||
227,
|
|
||||||
43.4592,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4397,
|
|
||||||
227,
|
|
||||||
31.6739,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
117.1136,
|
|
||||||
227,
|
|
||||||
16.6108,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
133.7244,
|
|
||||||
227,
|
|
||||||
28.1053,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.1009,
|
|
||||||
288,
|
|
||||||
37.8795,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.9805,
|
|
||||||
288,
|
|
||||||
43.4592,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4397,
|
|
||||||
288,
|
|
||||||
31.6739,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
117.1136,
|
|
||||||
288,
|
|
||||||
16.6108,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
133.7244,
|
|
||||||
288,
|
|
||||||
28.1053,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.1009,
|
|
||||||
349,
|
|
||||||
37.8795,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.9805,
|
|
||||||
349,
|
|
||||||
43.4592,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
85.4397,
|
|
||||||
349,
|
|
||||||
31.6739,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
117.1136,
|
|
||||||
349,
|
|
||||||
16.6108,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
133.7244,
|
|
||||||
349,
|
|
||||||
28.1053,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "trips-admin",
|
|
||||||
"viewportWidth": 690,
|
|
||||||
"width": 690,
|
|
||||||
"height": 442,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
16.3202,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
16.3202,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
375,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.7536,
|
|
||||||
86,
|
|
||||||
22.8057,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.5593,
|
|
||||||
86,
|
|
||||||
26.0711,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.6304,
|
|
||||||
86,
|
|
||||||
19.1757,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.8062,
|
|
||||||
86,
|
|
||||||
10.3601,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.1662,
|
|
||||||
86,
|
|
||||||
16.0802,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.7536,
|
|
||||||
119,
|
|
||||||
22.8057,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.5593,
|
|
||||||
119,
|
|
||||||
26.0711,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.6304,
|
|
||||||
119,
|
|
||||||
19.1757,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.8062,
|
|
||||||
119,
|
|
||||||
10.3601,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.1662,
|
|
||||||
119,
|
|
||||||
16.0802,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.7536,
|
|
||||||
180,
|
|
||||||
22.8057,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.5593,
|
|
||||||
180,
|
|
||||||
26.0711,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.6304,
|
|
||||||
180,
|
|
||||||
19.1757,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.8062,
|
|
||||||
180,
|
|
||||||
10.3601,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.1662,
|
|
||||||
180,
|
|
||||||
16.0802,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.7536,
|
|
||||||
241,
|
|
||||||
22.8057,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.5593,
|
|
||||||
241,
|
|
||||||
26.0711,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.6304,
|
|
||||||
241,
|
|
||||||
19.1757,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.8062,
|
|
||||||
241,
|
|
||||||
10.3601,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.1662,
|
|
||||||
241,
|
|
||||||
16.0802,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.7536,
|
|
||||||
302,
|
|
||||||
22.8057,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.5593,
|
|
||||||
302,
|
|
||||||
26.0711,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.6304,
|
|
||||||
302,
|
|
||||||
19.1757,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.8062,
|
|
||||||
302,
|
|
||||||
10.3601,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.1662,
|
|
||||||
302,
|
|
||||||
16.0802,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.7536,
|
|
||||||
363,
|
|
||||||
22.8057,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.5593,
|
|
||||||
363,
|
|
||||||
26.0711,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.6304,
|
|
||||||
363,
|
|
||||||
19.1757,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.8062,
|
|
||||||
363,
|
|
||||||
10.3601,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.1662,
|
|
||||||
363,
|
|
||||||
16.0802,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "trips-admin",
|
|
||||||
"viewportWidth": 950,
|
|
||||||
"width": 950,
|
|
||||||
"height": 418,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
11.8536,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
11.8536,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
351,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2,
|
|
||||||
86,
|
|
||||||
23.523,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.523,
|
|
||||||
86,
|
|
||||||
26.574,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.097,
|
|
||||||
86,
|
|
||||||
20.1382,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.2352,
|
|
||||||
86,
|
|
||||||
11.9046,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
84.1398,
|
|
||||||
86,
|
|
||||||
13.8602,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2,
|
|
||||||
124,
|
|
||||||
23.523,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.523,
|
|
||||||
124,
|
|
||||||
26.574,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.097,
|
|
||||||
124,
|
|
||||||
20.1382,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.2352,
|
|
||||||
124,
|
|
||||||
11.9046,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
84.1398,
|
|
||||||
124,
|
|
||||||
13.8602,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2,
|
|
||||||
179,
|
|
||||||
23.523,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.523,
|
|
||||||
179,
|
|
||||||
26.574,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.097,
|
|
||||||
179,
|
|
||||||
20.1382,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.2352,
|
|
||||||
179,
|
|
||||||
11.9046,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
84.1398,
|
|
||||||
179,
|
|
||||||
13.8602,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2,
|
|
||||||
234,
|
|
||||||
23.523,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.523,
|
|
||||||
234,
|
|
||||||
26.574,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.097,
|
|
||||||
234,
|
|
||||||
20.1382,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.2352,
|
|
||||||
234,
|
|
||||||
11.9046,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
84.1398,
|
|
||||||
234,
|
|
||||||
13.8602,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2,
|
|
||||||
289,
|
|
||||||
23.523,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.523,
|
|
||||||
289,
|
|
||||||
26.574,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.097,
|
|
||||||
289,
|
|
||||||
20.1382,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.2352,
|
|
||||||
289,
|
|
||||||
11.9046,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
84.1398,
|
|
||||||
289,
|
|
||||||
13.8602,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2,
|
|
||||||
344,
|
|
||||||
23.523,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.523,
|
|
||||||
344,
|
|
||||||
26.574,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.097,
|
|
||||||
344,
|
|
||||||
20.1382,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
72.2352,
|
|
||||||
344,
|
|
||||||
11.9046,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
84.1398,
|
|
||||||
344,
|
|
||||||
13.8602,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "39a325f430c84bb51960a684759a8f0c"
|
|
||||||
}
|
|
||||||
@@ -1,725 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "trips-history",
|
|
||||||
"viewportWidth": 317,
|
|
||||||
"width": 317,
|
|
||||||
"height": 300,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
26,
|
|
||||||
100,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
61,
|
|
||||||
100,
|
|
||||||
239,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.1009,
|
|
||||||
74,
|
|
||||||
35.2326,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
39.3336,
|
|
||||||
74,
|
|
||||||
40.4278,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
79.7614,
|
|
||||||
74,
|
|
||||||
29.4657,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.2271,
|
|
||||||
74,
|
|
||||||
37.1402,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
146.3673,
|
|
||||||
74,
|
|
||||||
15.4623,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.1009,
|
|
||||||
105,
|
|
||||||
35.2326,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
39.3336,
|
|
||||||
105,
|
|
||||||
40.4278,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
79.7614,
|
|
||||||
105,
|
|
||||||
29.4657,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.2271,
|
|
||||||
105,
|
|
||||||
37.1402,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
146.3673,
|
|
||||||
105,
|
|
||||||
15.4623,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.1009,
|
|
||||||
138,
|
|
||||||
35.2326,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
39.3336,
|
|
||||||
138,
|
|
||||||
40.4278,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
79.7614,
|
|
||||||
138,
|
|
||||||
29.4657,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.2271,
|
|
||||||
138,
|
|
||||||
37.1402,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
146.3673,
|
|
||||||
138,
|
|
||||||
15.4623,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.1009,
|
|
||||||
172,
|
|
||||||
35.2326,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
39.3336,
|
|
||||||
172,
|
|
||||||
40.4278,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
79.7614,
|
|
||||||
172,
|
|
||||||
29.4657,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.2271,
|
|
||||||
172,
|
|
||||||
37.1402,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
146.3673,
|
|
||||||
172,
|
|
||||||
15.4623,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.1009,
|
|
||||||
205,
|
|
||||||
35.2326,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
39.3336,
|
|
||||||
205,
|
|
||||||
40.4278,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
79.7614,
|
|
||||||
205,
|
|
||||||
29.4657,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.2271,
|
|
||||||
205,
|
|
||||||
37.1402,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
146.3673,
|
|
||||||
205,
|
|
||||||
15.4623,
|
|
||||||
34,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
4.1009,
|
|
||||||
239,
|
|
||||||
35.2326,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
39.3336,
|
|
||||||
239,
|
|
||||||
40.4278,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
79.7614,
|
|
||||||
239,
|
|
||||||
29.4657,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
109.2271,
|
|
||||||
239,
|
|
||||||
37.1402,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
146.3673,
|
|
||||||
239,
|
|
||||||
15.4623,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "trips-history",
|
|
||||||
"viewportWidth": 690,
|
|
||||||
"width": 690,
|
|
||||||
"height": 312,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
16.6033,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
16.6033,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
245,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.7536,
|
|
||||||
86,
|
|
||||||
21.0417,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.7953,
|
|
||||||
86,
|
|
||||||
24.0534,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.8487,
|
|
||||||
86,
|
|
||||||
17.6925,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.5412,
|
|
||||||
86,
|
|
||||||
22.1445,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.6857,
|
|
||||||
86,
|
|
||||||
9.5607,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.7536,
|
|
||||||
119,
|
|
||||||
21.0417,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.7953,
|
|
||||||
119,
|
|
||||||
24.0534,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.8487,
|
|
||||||
119,
|
|
||||||
17.6925,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.5412,
|
|
||||||
119,
|
|
||||||
22.1445,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.6857,
|
|
||||||
119,
|
|
||||||
9.5607,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.7536,
|
|
||||||
154,
|
|
||||||
21.0417,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.7953,
|
|
||||||
154,
|
|
||||||
24.0534,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.8487,
|
|
||||||
154,
|
|
||||||
17.6925,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.5412,
|
|
||||||
154,
|
|
||||||
22.1445,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.6857,
|
|
||||||
154,
|
|
||||||
9.5607,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.7536,
|
|
||||||
189,
|
|
||||||
21.0417,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.7953,
|
|
||||||
189,
|
|
||||||
24.0534,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.8487,
|
|
||||||
189,
|
|
||||||
17.6925,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.5412,
|
|
||||||
189,
|
|
||||||
22.1445,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.6857,
|
|
||||||
189,
|
|
||||||
9.5607,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.7536,
|
|
||||||
224,
|
|
||||||
21.0417,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.7953,
|
|
||||||
224,
|
|
||||||
24.0534,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.8487,
|
|
||||||
224,
|
|
||||||
17.6925,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.5412,
|
|
||||||
224,
|
|
||||||
22.1445,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.6857,
|
|
||||||
224,
|
|
||||||
9.5607,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.7536,
|
|
||||||
259,
|
|
||||||
21.0417,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.7953,
|
|
||||||
259,
|
|
||||||
24.0534,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.8487,
|
|
||||||
259,
|
|
||||||
17.6925,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.5412,
|
|
||||||
259,
|
|
||||||
22.1445,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.6857,
|
|
||||||
259,
|
|
||||||
9.5607,
|
|
||||||
35,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "trips-history",
|
|
||||||
"viewportWidth": 958,
|
|
||||||
"width": 958,
|
|
||||||
"height": 355,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
11.9585,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
11.9585,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
288,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9833,
|
|
||||||
86,
|
|
||||||
21.1541,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.1374,
|
|
||||||
86,
|
|
||||||
23.8974,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.0348,
|
|
||||||
86,
|
|
||||||
18.1106,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.1455,
|
|
||||||
86,
|
|
||||||
22.1604,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.3059,
|
|
||||||
86,
|
|
||||||
10.7108,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9833,
|
|
||||||
124,
|
|
||||||
21.1541,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.1374,
|
|
||||||
124,
|
|
||||||
23.8974,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.0348,
|
|
||||||
124,
|
|
||||||
18.1106,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.1455,
|
|
||||||
124,
|
|
||||||
22.1604,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.3059,
|
|
||||||
124,
|
|
||||||
10.7108,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9833,
|
|
||||||
167,
|
|
||||||
21.1541,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.1374,
|
|
||||||
167,
|
|
||||||
23.8974,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.0348,
|
|
||||||
167,
|
|
||||||
18.1106,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.1455,
|
|
||||||
167,
|
|
||||||
22.1604,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.3059,
|
|
||||||
167,
|
|
||||||
10.7108,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9833,
|
|
||||||
209,
|
|
||||||
21.1541,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.1374,
|
|
||||||
209,
|
|
||||||
23.8974,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.0348,
|
|
||||||
209,
|
|
||||||
18.1106,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.1455,
|
|
||||||
209,
|
|
||||||
22.1604,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.3059,
|
|
||||||
209,
|
|
||||||
10.7108,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9833,
|
|
||||||
252,
|
|
||||||
21.1541,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.1374,
|
|
||||||
252,
|
|
||||||
23.8974,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.0348,
|
|
||||||
252,
|
|
||||||
18.1106,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.1455,
|
|
||||||
252,
|
|
||||||
22.1604,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.3059,
|
|
||||||
252,
|
|
||||||
10.7108,
|
|
||||||
43,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9833,
|
|
||||||
294,
|
|
||||||
21.1541,
|
|
||||||
42,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
23.1374,
|
|
||||||
294,
|
|
||||||
23.8974,
|
|
||||||
42,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.0348,
|
|
||||||
294,
|
|
||||||
18.1106,
|
|
||||||
42,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
65.1455,
|
|
||||||
294,
|
|
||||||
22.1604,
|
|
||||||
42,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.3059,
|
|
||||||
294,
|
|
||||||
10.7108,
|
|
||||||
42,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "6b54a0afbb4863895e318916b1fdca67"
|
|
||||||
}
|
|
||||||
@@ -1,767 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "users",
|
|
||||||
"viewportWidth": 351,
|
|
||||||
"width": 351,
|
|
||||||
"height": 549,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
26,
|
|
||||||
100,
|
|
||||||
19,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
53,
|
|
||||||
100,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
113,
|
|
||||||
100,
|
|
||||||
436,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
126,
|
|
||||||
92.5926,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
186,
|
|
||||||
37.362,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.0657,
|
|
||||||
186,
|
|
||||||
26.429,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
67.4947,
|
|
||||||
186,
|
|
||||||
36.1779,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
103.6725,
|
|
||||||
186,
|
|
||||||
23.62,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
127.2926,
|
|
||||||
186,
|
|
||||||
18.8613,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
217,
|
|
||||||
37.362,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.0657,
|
|
||||||
217,
|
|
||||||
26.429,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
67.4947,
|
|
||||||
217,
|
|
||||||
36.1779,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
103.6725,
|
|
||||||
217,
|
|
||||||
23.62,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
127.2926,
|
|
||||||
217,
|
|
||||||
18.8613,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
278,
|
|
||||||
37.362,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.0657,
|
|
||||||
278,
|
|
||||||
26.429,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
67.4947,
|
|
||||||
278,
|
|
||||||
36.1779,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
103.6725,
|
|
||||||
278,
|
|
||||||
23.62,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
127.2926,
|
|
||||||
278,
|
|
||||||
18.8613,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
339,
|
|
||||||
37.362,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.0657,
|
|
||||||
339,
|
|
||||||
26.429,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
67.4947,
|
|
||||||
339,
|
|
||||||
36.1779,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
103.6725,
|
|
||||||
339,
|
|
||||||
23.62,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
127.2926,
|
|
||||||
339,
|
|
||||||
18.8613,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
400,
|
|
||||||
37.362,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.0657,
|
|
||||||
400,
|
|
||||||
26.429,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
67.4947,
|
|
||||||
400,
|
|
||||||
36.1779,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
103.6725,
|
|
||||||
400,
|
|
||||||
23.62,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
127.2926,
|
|
||||||
400,
|
|
||||||
18.8613,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
461,
|
|
||||||
37.362,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
41.0657,
|
|
||||||
461,
|
|
||||||
26.429,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
67.4947,
|
|
||||||
461,
|
|
||||||
36.1779,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
103.6725,
|
|
||||||
461,
|
|
||||||
23.62,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
127.2926,
|
|
||||||
461,
|
|
||||||
18.8613,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "users",
|
|
||||||
"viewportWidth": 736,
|
|
||||||
"width": 736,
|
|
||||||
"height": 502,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
12.6741,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
12.6741,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.2479,
|
|
||||||
4,
|
|
||||||
18.7521,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
435,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
86,
|
|
||||||
94.837,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
146,
|
|
||||||
24.3079,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.8894,
|
|
||||||
146,
|
|
||||||
18.6481,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.5375,
|
|
||||||
146,
|
|
||||||
23.5628,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.1003,
|
|
||||||
146,
|
|
||||||
15.6568,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
84.7571,
|
|
||||||
146,
|
|
||||||
12.6613,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
179,
|
|
||||||
24.3079,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.8894,
|
|
||||||
179,
|
|
||||||
18.6481,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.5375,
|
|
||||||
179,
|
|
||||||
23.5628,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.1003,
|
|
||||||
179,
|
|
||||||
15.6568,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
84.7571,
|
|
||||||
179,
|
|
||||||
12.6613,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
240,
|
|
||||||
24.3079,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.8894,
|
|
||||||
240,
|
|
||||||
18.6481,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.5375,
|
|
||||||
240,
|
|
||||||
23.5628,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.1003,
|
|
||||||
240,
|
|
||||||
15.6568,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
84.7571,
|
|
||||||
240,
|
|
||||||
12.6613,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
301,
|
|
||||||
24.3079,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.8894,
|
|
||||||
301,
|
|
||||||
18.6481,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.5375,
|
|
||||||
301,
|
|
||||||
23.5628,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.1003,
|
|
||||||
301,
|
|
||||||
15.6568,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
84.7571,
|
|
||||||
301,
|
|
||||||
12.6613,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
362,
|
|
||||||
24.3079,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.8894,
|
|
||||||
362,
|
|
||||||
18.6481,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.5375,
|
|
||||||
362,
|
|
||||||
23.5628,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.1003,
|
|
||||||
362,
|
|
||||||
15.6568,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
84.7571,
|
|
||||||
362,
|
|
||||||
12.6613,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
423,
|
|
||||||
24.3079,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
26.8894,
|
|
||||||
423,
|
|
||||||
18.6481,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
45.5375,
|
|
||||||
423,
|
|
||||||
23.5628,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
69.1003,
|
|
||||||
423,
|
|
||||||
15.6568,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
84.7571,
|
|
||||||
423,
|
|
||||||
12.6613,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "users",
|
|
||||||
"viewportWidth": 996,
|
|
||||||
"width": 996,
|
|
||||||
"height": 505,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
9.3656,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
30,
|
|
||||||
9.3656,
|
|
||||||
21,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
86.5446,
|
|
||||||
10,
|
|
||||||
13.4554,
|
|
||||||
32,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
67,
|
|
||||||
100,
|
|
||||||
438,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
86,
|
|
||||||
96.1847,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
138,
|
|
||||||
25.3655,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.2732,
|
|
||||||
138,
|
|
||||||
20.4302,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.7033,
|
|
||||||
138,
|
|
||||||
22.8571,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.5604,
|
|
||||||
138,
|
|
||||||
15.9011,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
86.4615,
|
|
||||||
138,
|
|
||||||
11.6309,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
176,
|
|
||||||
25.3655,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.2732,
|
|
||||||
176,
|
|
||||||
20.4302,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.7033,
|
|
||||||
176,
|
|
||||||
22.8571,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.5604,
|
|
||||||
176,
|
|
||||||
15.9011,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
86.4615,
|
|
||||||
176,
|
|
||||||
11.6309,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
238,
|
|
||||||
25.3655,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.2732,
|
|
||||||
238,
|
|
||||||
20.4302,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.7033,
|
|
||||||
238,
|
|
||||||
22.8571,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.5604,
|
|
||||||
238,
|
|
||||||
15.9011,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
86.4615,
|
|
||||||
238,
|
|
||||||
11.6309,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
300,
|
|
||||||
25.3655,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.2732,
|
|
||||||
300,
|
|
||||||
20.4302,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.7033,
|
|
||||||
300,
|
|
||||||
22.8571,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.5604,
|
|
||||||
300,
|
|
||||||
15.9011,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
86.4615,
|
|
||||||
300,
|
|
||||||
11.6309,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
362,
|
|
||||||
25.3655,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.2732,
|
|
||||||
362,
|
|
||||||
20.4302,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.7033,
|
|
||||||
362,
|
|
||||||
22.8571,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.5604,
|
|
||||||
362,
|
|
||||||
15.9011,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
86.4615,
|
|
||||||
362,
|
|
||||||
11.6309,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
424,
|
|
||||||
25.3655,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.2732,
|
|
||||||
424,
|
|
||||||
20.4302,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
47.7033,
|
|
||||||
424,
|
|
||||||
22.8571,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
70.5604,
|
|
||||||
424,
|
|
||||||
15.9011,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
86.4615,
|
|
||||||
424,
|
|
||||||
11.6309,
|
|
||||||
62,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "53e8df6c8f8bf975b3b88bfca3bbd804"
|
|
||||||
}
|
|
||||||
@@ -1,746 +0,0 @@
|
|||||||
{
|
|
||||||
"breakpoints": {
|
|
||||||
"375": {
|
|
||||||
"name": "vehicles",
|
|
||||||
"viewportWidth": 351,
|
|
||||||
"width": 351,
|
|
||||||
"height": 530,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
100,
|
|
||||||
22,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
34,
|
|
||||||
100,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
94,
|
|
||||||
100,
|
|
||||||
436,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
107,
|
|
||||||
92.5926,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
167,
|
|
||||||
23.9583,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.662,
|
|
||||||
167,
|
|
||||||
24.4168,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.0789,
|
|
||||||
167,
|
|
||||||
29.042,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.1209,
|
|
||||||
167,
|
|
||||||
18.8435,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
99.9644,
|
|
||||||
167,
|
|
||||||
46.1895,
|
|
||||||
31,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
197,
|
|
||||||
23.9583,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.662,
|
|
||||||
197,
|
|
||||||
24.4168,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.0789,
|
|
||||||
197,
|
|
||||||
29.042,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.1209,
|
|
||||||
197,
|
|
||||||
18.8435,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
99.9644,
|
|
||||||
197,
|
|
||||||
46.1895,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
258,
|
|
||||||
23.9583,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.662,
|
|
||||||
258,
|
|
||||||
24.4168,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.0789,
|
|
||||||
258,
|
|
||||||
29.042,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.1209,
|
|
||||||
258,
|
|
||||||
18.8435,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
99.9644,
|
|
||||||
258,
|
|
||||||
46.1895,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
319,
|
|
||||||
23.9583,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.662,
|
|
||||||
319,
|
|
||||||
24.4168,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.0789,
|
|
||||||
319,
|
|
||||||
29.042,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.1209,
|
|
||||||
319,
|
|
||||||
18.8435,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
99.9644,
|
|
||||||
319,
|
|
||||||
46.1895,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
380,
|
|
||||||
23.9583,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.662,
|
|
||||||
380,
|
|
||||||
24.4168,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.0789,
|
|
||||||
380,
|
|
||||||
29.042,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.1209,
|
|
||||||
380,
|
|
||||||
18.8435,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
99.9644,
|
|
||||||
380,
|
|
||||||
46.1895,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3.7037,
|
|
||||||
441,
|
|
||||||
23.9583,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
27.662,
|
|
||||||
441,
|
|
||||||
24.4168,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
52.0789,
|
|
||||||
441,
|
|
||||||
29.042,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
81.1209,
|
|
||||||
441,
|
|
||||||
18.8435,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
99.9644,
|
|
||||||
441,
|
|
||||||
46.1895,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"768": {
|
|
||||||
"name": "vehicles",
|
|
||||||
"viewportWidth": 736,
|
|
||||||
"width": 736,
|
|
||||||
"height": 495,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
7,
|
|
||||||
10.1478,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
82.6427,
|
|
||||||
0,
|
|
||||||
17.3573,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
60,
|
|
||||||
100,
|
|
||||||
435,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
79,
|
|
||||||
94.837,
|
|
||||||
44,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
139,
|
|
||||||
16.4126,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.9941,
|
|
||||||
139,
|
|
||||||
16.5039,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.498,
|
|
||||||
139,
|
|
||||||
19.5058,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
55.0038,
|
|
||||||
139,
|
|
||||||
12.8843,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
67.8881,
|
|
||||||
139,
|
|
||||||
29.5304,
|
|
||||||
33,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
172,
|
|
||||||
16.4126,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.9941,
|
|
||||||
172,
|
|
||||||
16.5039,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.498,
|
|
||||||
172,
|
|
||||||
19.5058,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
55.0038,
|
|
||||||
172,
|
|
||||||
12.8843,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
67.8881,
|
|
||||||
172,
|
|
||||||
29.5304,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
233,
|
|
||||||
16.4126,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.9941,
|
|
||||||
233,
|
|
||||||
16.5039,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.498,
|
|
||||||
233,
|
|
||||||
19.5058,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
55.0038,
|
|
||||||
233,
|
|
||||||
12.8843,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
67.8881,
|
|
||||||
233,
|
|
||||||
29.5304,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
294,
|
|
||||||
16.4126,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.9941,
|
|
||||||
294,
|
|
||||||
16.5039,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.498,
|
|
||||||
294,
|
|
||||||
19.5058,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
55.0038,
|
|
||||||
294,
|
|
||||||
12.8843,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
67.8881,
|
|
||||||
294,
|
|
||||||
29.5304,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
355,
|
|
||||||
16.4126,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.9941,
|
|
||||||
355,
|
|
||||||
16.5039,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.498,
|
|
||||||
355,
|
|
||||||
19.5058,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
55.0038,
|
|
||||||
355,
|
|
||||||
12.8843,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
67.8881,
|
|
||||||
355,
|
|
||||||
29.5304,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2.5815,
|
|
||||||
416,
|
|
||||||
16.4126,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
18.9941,
|
|
||||||
416,
|
|
||||||
16.5039,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
35.498,
|
|
||||||
416,
|
|
||||||
19.5058,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
55.0038,
|
|
||||||
416,
|
|
||||||
12.8843,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
67.8881,
|
|
||||||
416,
|
|
||||||
29.5304,
|
|
||||||
61,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"1280": {
|
|
||||||
"name": "vehicles",
|
|
||||||
"viewportWidth": 996,
|
|
||||||
"width": 996,
|
|
||||||
"height": 451,
|
|
||||||
"bones": [
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
7.4987,
|
|
||||||
26,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
87.5753,
|
|
||||||
0,
|
|
||||||
12.4247,
|
|
||||||
32,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
48,
|
|
||||||
100,
|
|
||||||
403,
|
|
||||||
10,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
67,
|
|
||||||
96.1847,
|
|
||||||
36,
|
|
||||||
8
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
119,
|
|
||||||
18.3531,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.2607,
|
|
||||||
119,
|
|
||||||
18.2762,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.537,
|
|
||||||
119,
|
|
||||||
21.1785,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
59.7154,
|
|
||||||
119,
|
|
||||||
14.7841,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
74.4996,
|
|
||||||
119,
|
|
||||||
23.5928,
|
|
||||||
38,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
157,
|
|
||||||
18.3531,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.2607,
|
|
||||||
157,
|
|
||||||
18.2762,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.537,
|
|
||||||
157,
|
|
||||||
21.1785,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
59.7154,
|
|
||||||
157,
|
|
||||||
14.7841,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
74.4996,
|
|
||||||
157,
|
|
||||||
23.5928,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
212,
|
|
||||||
18.3531,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.2607,
|
|
||||||
212,
|
|
||||||
18.2762,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.537,
|
|
||||||
212,
|
|
||||||
21.1785,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
59.7154,
|
|
||||||
212,
|
|
||||||
14.7841,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
74.4996,
|
|
||||||
212,
|
|
||||||
23.5928,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
267,
|
|
||||||
18.3531,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.2607,
|
|
||||||
267,
|
|
||||||
18.2762,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.537,
|
|
||||||
267,
|
|
||||||
21.1785,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
59.7154,
|
|
||||||
267,
|
|
||||||
14.7841,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
74.4996,
|
|
||||||
267,
|
|
||||||
23.5928,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
322,
|
|
||||||
18.3531,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.2607,
|
|
||||||
322,
|
|
||||||
18.2762,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.537,
|
|
||||||
322,
|
|
||||||
21.1785,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
59.7154,
|
|
||||||
322,
|
|
||||||
14.7841,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
74.4996,
|
|
||||||
322,
|
|
||||||
23.5928,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1.9076,
|
|
||||||
377,
|
|
||||||
18.3531,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
20.2607,
|
|
||||||
377,
|
|
||||||
18.2762,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
38.537,
|
|
||||||
377,
|
|
||||||
21.1785,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
59.7154,
|
|
||||||
377,
|
|
||||||
14.7841,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
],
|
|
||||||
[
|
|
||||||
74.4996,
|
|
||||||
377,
|
|
||||||
23.5928,
|
|
||||||
55,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_hash": "567bad6080dc9ba9767c6e40a88559b9"
|
|
||||||
}
|
|
||||||
@@ -41,6 +41,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Danger card — highlighted border for alert/important cards */
|
||||||
|
.admin-card-danger {
|
||||||
|
border: 2px solid var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
Badges
|
Badges
|
||||||
============================================================================ */
|
============================================================================ */
|
||||||
@@ -123,6 +128,16 @@
|
|||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-badge-incoming {
|
||||||
|
background: var(--success-soft);
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-badge-outgoing {
|
||||||
|
background: var(--info-soft);
|
||||||
|
color: var(--info);
|
||||||
|
}
|
||||||
|
|
||||||
/* Status Badges - Leave Requests */
|
/* Status Badges - Leave Requests */
|
||||||
.badge-pending {
|
.badge-pending {
|
||||||
background: color-mix(in srgb, var(--warning) 15%, transparent);
|
background: color-mix(in srgb, var(--warning) 15%, transparent);
|
||||||
@@ -716,6 +731,7 @@
|
|||||||
.admin-kpi-grid {
|
.admin-kpi-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.875rem;
|
gap: 0.875rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-kpi-4 {
|
.admin-kpi-4 {
|
||||||
@@ -923,3 +939,163 @@
|
|||||||
max-height: 80px;
|
max-height: 80px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
Search Bar & Filters
|
||||||
|
============================================================================ */
|
||||||
|
|
||||||
|
.admin-search-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-search-bar .admin-form-input,
|
||||||
|
.admin-search-bar .admin-form-select {
|
||||||
|
flex: 1 1 140px;
|
||||||
|
min-width: 140px;
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-search-bar .react-datepicker-wrapper {
|
||||||
|
flex: 1 1 140px;
|
||||||
|
min-width: 140px;
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-search-bar .admin-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-search {
|
||||||
|
position: relative;
|
||||||
|
flex: 1 1 200px;
|
||||||
|
min-width: 180px;
|
||||||
|
max-width: 320px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-search svg {
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-search .admin-form-input {
|
||||||
|
padding-left: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.admin-search-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-search-bar .admin-form-input,
|
||||||
|
.admin-search-bar .admin-form-select {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-search-bar .react-datepicker-wrapper {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-search {
|
||||||
|
max-width: 100%;
|
||||||
|
flex: 1 1 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.admin-search-bar .admin-form-input,
|
||||||
|
.admin-search-bar .admin-form-select {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-search-bar .react-datepicker-wrapper {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
Item Picker (Warehouse)
|
||||||
|
============================================================================ */
|
||||||
|
|
||||||
|
.admin-item-picker-list {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
max-height: 240px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 var(--border-radius-sm) var(--border-radius-sm);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
padding: 4px 0;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-item-picker-list::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-item-picker-list::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-item-picker-list::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border-color);
|
||||||
|
border-radius: 99px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-item-picker-list::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-item-picker-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background var(--transition);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-item-picker-item:hover,
|
||||||
|
.admin-item-picker-item.active {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-item-picker-name {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-item-picker-number {
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-item-picker-qty {
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { forwardRef, useMemo } from "react";
|
import { forwardRef } from "react";
|
||||||
import DatePicker, { registerLocale } from "react-datepicker";
|
import DatePicker, { registerLocale } from "react-datepicker";
|
||||||
import { cs } from "date-fns/locale";
|
import { cs } from "date-fns/locale";
|
||||||
import { parse, format } from "date-fns";
|
import { parse, format } from "date-fns";
|
||||||
@@ -124,7 +124,7 @@ export default function AdminDatePicker({
|
|||||||
disabled,
|
disabled,
|
||||||
placeholder,
|
placeholder,
|
||||||
}: AdminDatePickerProps) {
|
}: AdminDatePickerProps) {
|
||||||
const useNative = useMemo(() => isTouchDevice(), []);
|
const useNative = isTouchDevice();
|
||||||
|
|
||||||
if (useNative) {
|
if (useNative) {
|
||||||
return (
|
return (
|
||||||
@@ -178,15 +178,12 @@ export default function AdminDatePicker({
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const customInput = useMemo(
|
const customInput = (
|
||||||
() => (
|
|
||||||
<CustomInput
|
<CustomInput
|
||||||
required={required}
|
required={required}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
),
|
|
||||||
[required, placeholder, disabled],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const commonProps = {
|
const commonProps = {
|
||||||
@@ -194,6 +191,7 @@ export default function AdminDatePicker({
|
|||||||
onChange: handleChange,
|
onChange: handleChange,
|
||||||
locale: "cs",
|
locale: "cs",
|
||||||
customInput,
|
customInput,
|
||||||
|
placeholderText: placeholder,
|
||||||
minDate: parseMinMax(minDate),
|
minDate: parseMinMax(minDate),
|
||||||
maxDate: parseMinMax(maxDate),
|
maxDate: parseMinMax(maxDate),
|
||||||
popperPlacement: "bottom-start" as const,
|
popperPlacement: "bottom-start" as const,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
formatDate,
|
formatDate,
|
||||||
formatDatetime,
|
formatDatetime,
|
||||||
@@ -8,34 +8,13 @@ import {
|
|||||||
getLeaveTypeName,
|
getLeaveTypeName,
|
||||||
getLeaveTypeBadgeClass,
|
getLeaveTypeBadgeClass,
|
||||||
} from "../utils/attendanceHelpers";
|
} from "../utils/attendanceHelpers";
|
||||||
|
import type { AttendanceRecord as HookAttendanceRecord } from "../hooks/useAttendanceAdmin";
|
||||||
|
|
||||||
interface ProjectLog {
|
interface AttendanceRecord extends HookAttendanceRecord {
|
||||||
id?: number;
|
|
||||||
project_id?: number;
|
|
||||||
project_name?: string;
|
|
||||||
started_at?: string;
|
|
||||||
ended_at?: string | null;
|
|
||||||
hours?: string | number | null;
|
|
||||||
minutes?: string | number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AttendanceRecord {
|
|
||||||
id: number;
|
|
||||||
shift_date: string;
|
|
||||||
user_name: string;
|
|
||||||
leave_type?: string;
|
|
||||||
leave_hours?: number;
|
|
||||||
arrival_time?: string | null;
|
|
||||||
departure_time?: string | null;
|
|
||||||
break_start?: string | null;
|
|
||||||
break_end?: string | null;
|
|
||||||
arrival_lat?: number | string | null;
|
arrival_lat?: number | string | null;
|
||||||
arrival_lng?: number | string | null;
|
arrival_lng?: number | string | null;
|
||||||
departure_lat?: number | string | null;
|
departure_lat?: number | string | null;
|
||||||
departure_lng?: number | string | null;
|
departure_lng?: number | string | null;
|
||||||
project_name?: string;
|
|
||||||
project_logs?: ProjectLog[];
|
|
||||||
notes?: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AttendanceShiftTableProps {
|
interface AttendanceShiftTableProps {
|
||||||
@@ -51,7 +30,7 @@ function formatBreak(record: AttendanceRecord): string {
|
|||||||
if (record.break_start) {
|
if (record.break_start) {
|
||||||
return `${formatTime(record.break_start)} - ?`;
|
return `${formatTime(record.break_start)} - ?`;
|
||||||
}
|
}
|
||||||
return "\u2014";
|
return "—";
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderProjectCell(record: AttendanceRecord): React.ReactNode {
|
function renderProjectCell(record: AttendanceRecord): React.ReactNode {
|
||||||
@@ -97,7 +76,7 @@ function renderProjectCell(record: AttendanceRecord): React.ReactNode {
|
|||||||
>
|
>
|
||||||
{log.project_name || `#${log.project_id}`}{" "}
|
{log.project_name || `#${log.project_id}`}{" "}
|
||||||
{durationValid
|
{durationValid
|
||||||
? `(${h}:${String(m).padStart(2, "0")}h${isActive ? " \u25B8" : ""})`
|
? `(${h}:${String(m).padStart(2, "0")}h${isActive ? " ▸" : ""})`
|
||||||
: "—"}
|
: "—"}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@@ -115,7 +94,7 @@ function renderProjectCell(record: AttendanceRecord): React.ReactNode {
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return "\u2014";
|
return "—";
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AttendanceShiftTable({
|
export default function AttendanceShiftTable({
|
||||||
@@ -156,7 +135,10 @@ export default function AttendanceShiftTable({
|
|||||||
const workMinutes = isLeave
|
const workMinutes = isLeave
|
||||||
? (record.leave_hours != null ? Number(record.leave_hours) : 8) *
|
? (record.leave_hours != null ? Number(record.leave_hours) : 8) *
|
||||||
60
|
60
|
||||||
: calculateWorkMinutes(record);
|
: calculateWorkMinutes({
|
||||||
|
...record,
|
||||||
|
notes: record.notes ?? undefined,
|
||||||
|
});
|
||||||
const hasLocation =
|
const hasLocation =
|
||||||
(record.arrival_lat && record.arrival_lng) ||
|
(record.arrival_lat && record.arrival_lng) ||
|
||||||
(record.departure_lat && record.departure_lng);
|
(record.departure_lat && record.departure_lng);
|
||||||
@@ -173,18 +155,16 @@ export default function AttendanceShiftTable({
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="admin-mono">
|
<td className="admin-mono">
|
||||||
{isLeave ? "\u2014" : formatDatetime(record.arrival_time)}
|
{isLeave ? "—" : formatDatetime(record.arrival_time)}
|
||||||
</td>
|
</td>
|
||||||
<td className="admin-mono">
|
<td className="admin-mono">
|
||||||
{isLeave ? "\u2014" : formatBreak(record)}
|
{isLeave ? "—" : formatBreak(record)}
|
||||||
</td>
|
</td>
|
||||||
<td className="admin-mono">
|
<td className="admin-mono">
|
||||||
{isLeave ? "\u2014" : formatDatetime(record.departure_time)}
|
{isLeave ? "—" : formatDatetime(record.departure_time)}
|
||||||
</td>
|
</td>
|
||||||
<td className="admin-mono">
|
<td className="admin-mono">
|
||||||
{workMinutes > 0
|
{workMinutes > 0 ? `${formatMinutes(workMinutes)} h` : "—"}
|
||||||
? `${formatMinutes(workMinutes)} h`
|
|
||||||
: "\u2014"}
|
|
||||||
</td>
|
</td>
|
||||||
<td>{renderProjectCell(record)}</td>
|
<td>{renderProjectCell(record)}</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -195,10 +175,10 @@ export default function AttendanceShiftTable({
|
|||||||
title="Zobrazit polohu"
|
title="Zobrazit polohu"
|
||||||
aria-label="Zobrazit polohu"
|
aria-label="Zobrazit polohu"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">{"\uD83D\uDCCD"}</span>
|
<span aria-hidden="true">{"📍"}</span>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
"\u2014"
|
"—"
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ReactNode } from "react";
|
import { useEffect, type ReactNode } from "react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
|
||||||
interface ConfirmModalProps {
|
interface ConfirmModalProps {
|
||||||
@@ -91,6 +91,19 @@ export default function ConfirmModal({
|
|||||||
confirmVariant,
|
confirmVariant,
|
||||||
loading,
|
loading,
|
||||||
}: ConfirmModalProps) {
|
}: ConfirmModalProps) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
|
||||||
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
|
if (e.key === "Escape" && !loading) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [isOpen, loading, onClose]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
|
|||||||
118
src/admin/components/FormModal.tsx
Normal file
118
src/admin/components/FormModal.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { useEffect, type ReactNode, type FormEvent } from "react";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import useModalLock from "../hooks/useModalLock";
|
||||||
|
|
||||||
|
export interface FormModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit?: () => void; // called when user clicks primary button (only if provided)
|
||||||
|
title: string;
|
||||||
|
children: ReactNode; // modal body (form fields)
|
||||||
|
submitLabel?: string; // primary button text
|
||||||
|
cancelLabel?: string; // cancel button text
|
||||||
|
loading?: boolean;
|
||||||
|
hideFooter?: boolean; // for "view-only" modals
|
||||||
|
size?: "sm" | "md" | "lg"; // optional
|
||||||
|
closeOnBackdrop?: boolean; // default true
|
||||||
|
closeOnEsc?: boolean; // default true
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FormModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
submitLabel = "Uložit",
|
||||||
|
cancelLabel = "Zrušit",
|
||||||
|
loading = false,
|
||||||
|
hideFooter = false,
|
||||||
|
size = "md",
|
||||||
|
closeOnBackdrop = true,
|
||||||
|
closeOnEsc = true,
|
||||||
|
}: FormModalProps) {
|
||||||
|
useModalLock(isOpen);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen || !closeOnEsc) return;
|
||||||
|
|
||||||
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
|
if (e.key === "Escape" && !loading) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [isOpen, closeOnEsc, loading, onClose]);
|
||||||
|
|
||||||
|
const handleBackdropClick = () => {
|
||||||
|
if (closeOnBackdrop && !loading) onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrimaryClick = (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (onSubmit) onSubmit();
|
||||||
|
};
|
||||||
|
|
||||||
|
const titleId = "form-modal-title";
|
||||||
|
const modalClassName =
|
||||||
|
size === "lg" ? "admin-modal admin-modal-lg" : "admin-modal";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<motion.div
|
||||||
|
className="admin-modal-overlay"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<div className="admin-modal-backdrop" onClick={handleBackdropClick} />
|
||||||
|
<motion.div
|
||||||
|
className={modalClassName}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby={titleId}
|
||||||
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<div className="admin-modal-header">
|
||||||
|
<h2 id={titleId} className="admin-modal-title">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="admin-modal-body">{children}</div>
|
||||||
|
|
||||||
|
{!hideFooter && (
|
||||||
|
<div className="admin-modal-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => !loading && onClose()}
|
||||||
|
className="admin-btn admin-btn-secondary"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{cancelLabel}
|
||||||
|
</button>
|
||||||
|
{onSubmit && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handlePrimaryClick}
|
||||||
|
className="admin-btn admin-btn-primary"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? "Zpracování..." : submitLabel}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,8 +4,6 @@ import { projectFilesOptions } from "../lib/queries/projects";
|
|||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import ConfirmModal from "./ConfirmModal";
|
import ConfirmModal from "./ConfirmModal";
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
import { Skeleton } from "boneyard-js/react";
|
|
||||||
import ProjectFileManagerFixture from "../fixtures/ProjectFileManagerFixture";
|
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
@@ -472,13 +470,9 @@ export default function ProjectFileManager({
|
|||||||
|
|
||||||
if (filesLoading && items.length === 0 && !errorMessage) {
|
if (filesLoading && items.length === 0 && !errorMessage) {
|
||||||
return (
|
return (
|
||||||
<Skeleton
|
<div className="admin-loading">
|
||||||
name="project-file-manager"
|
<div className="admin-spinner" />
|
||||||
loading={filesLoading && items.length === 0}
|
</div>
|
||||||
fixture={<ProjectFileManagerFixture />}
|
|
||||||
>
|
|
||||||
<div />
|
|
||||||
</Skeleton>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,15 @@ export interface ShiftFormModalProps {
|
|||||||
|
|
||||||
function ProjectTimeStatus({ form, projectLogs }: ProjectTimeStatusProps) {
|
function ProjectTimeStatus({ form, projectLogs }: ProjectTimeStatusProps) {
|
||||||
const totalWork = calcFormWorkMinutes(form);
|
const totalWork = calcFormWorkMinutes(form);
|
||||||
const totalProject = calcProjectMinutesTotal(projectLogs);
|
const totalProject = calcProjectMinutesTotal(
|
||||||
|
projectLogs.map((l) => ({
|
||||||
|
...l,
|
||||||
|
project_id:
|
||||||
|
l.project_id !== "" && l.project_id != null
|
||||||
|
? Number(l.project_id)
|
||||||
|
: undefined,
|
||||||
|
})),
|
||||||
|
);
|
||||||
const remaining = totalWork - totalProject;
|
const remaining = totalWork - totalProject;
|
||||||
const hasLogs = projectLogs.some((l) => l.project_id);
|
const hasLogs = projectLogs.some((l) => l.project_id);
|
||||||
|
|
||||||
@@ -329,7 +337,6 @@ export default function ShiftFormModal({
|
|||||||
<option value="work">Práce</option>
|
<option value="work">Práce</option>
|
||||||
<option value="vacation">Dovolená</option>
|
<option value="vacation">Dovolená</option>
|
||||||
<option value="sick">Nemoc</option>
|
<option value="sick">Nemoc</option>
|
||||||
<option value="holiday">Svátek</option>
|
|
||||||
<option value="unpaid">Neplacené volno</option>
|
<option value="unpaid">Neplacené volno</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -330,11 +330,16 @@ const menuSections: MenuSection[] = [
|
|||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Sklad",
|
||||||
|
items: [
|
||||||
{
|
{
|
||||||
path: "/warehouse",
|
path: "/warehouse",
|
||||||
label: "Sklad",
|
label: "Přehled",
|
||||||
permission: "warehouse.view",
|
permission: "warehouse.view",
|
||||||
matchPrefix: "/warehouse",
|
end: true,
|
||||||
icon: (
|
icon: (
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -350,6 +355,176 @@ const menuSections: MenuSection[] = [
|
|||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/warehouse/items",
|
||||||
|
label: "Položky",
|
||||||
|
permission: "warehouse.view",
|
||||||
|
matchPrefix: "/warehouse/items",
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
|
||||||
|
<polyline points="3.27 6.96 12 12.01 20.73 6.96" />
|
||||||
|
<line x1="12" y1="22.08" x2="12" y2="12" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/warehouse/receipts",
|
||||||
|
label: "Příjmy",
|
||||||
|
permission: "warehouse.operate",
|
||||||
|
matchPrefix: "/warehouse/receipts",
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<polyline points="17 11 12 6 7 11" />
|
||||||
|
<line x1="12" y1="6" x2="12" y2="18" />
|
||||||
|
<path d="M5 19h14" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/warehouse/issues",
|
||||||
|
label: "Výdeje",
|
||||||
|
permission: "warehouse.operate",
|
||||||
|
matchPrefix: "/warehouse/issues",
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<polyline points="7 13 12 18 17 13" />
|
||||||
|
<line x1="12" y1="18" x2="12" y2="6" />
|
||||||
|
<path d="M5 5h14" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/warehouse/reservations",
|
||||||
|
label: "Rezervace",
|
||||||
|
permission: "warehouse.operate",
|
||||||
|
matchPrefix: "/warehouse/reservations",
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
|
||||||
|
<circle cx="9" cy="7" r="4" />
|
||||||
|
<line x1="19" y1="8" x2="19" y2="14" />
|
||||||
|
<line x1="22" y1="11" x2="16" y2="11" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/warehouse/inventory",
|
||||||
|
label: "Inventura",
|
||||||
|
permission: "warehouse.inventory",
|
||||||
|
matchPrefix: "/warehouse/inventory",
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M9 11l3 3L22 4" />
|
||||||
|
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/warehouse/reports",
|
||||||
|
label: "Reporty",
|
||||||
|
permission: "warehouse.view",
|
||||||
|
end: true,
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<line x1="18" y1="20" x2="18" y2="10" />
|
||||||
|
<line x1="12" y1="20" x2="12" y2="4" />
|
||||||
|
<line x1="6" y1="20" x2="6" y2="14" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/warehouse/categories",
|
||||||
|
label: "Kategorie",
|
||||||
|
permission: "warehouse.manage",
|
||||||
|
matchPrefix: "/warehouse/categories",
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<line x1="4" y1="21" x2="4" y2="14" />
|
||||||
|
<line x1="4" y1="10" x2="4" y2="3" />
|
||||||
|
<line x1="12" y1="21" x2="12" y2="12" />
|
||||||
|
<line x1="12" y1="8" x2="12" y2="3" />
|
||||||
|
<line x1="20" y1="21" x2="20" y2="16" />
|
||||||
|
<line x1="20" y1="12" x2="20" y2="3" />
|
||||||
|
<line x1="1" y1="14" x2="7" y2="14" />
|
||||||
|
<line x1="9" y1="8" x2="15" y2="8" />
|
||||||
|
<line x1="17" y1="16" x2="23" y2="16" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/warehouse/locations",
|
||||||
|
label: "Lokace",
|
||||||
|
permission: "warehouse.manage",
|
||||||
|
matchPrefix: "/warehouse/locations",
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<rect x="3" y="3" width="7" height="7" rx="1" />
|
||||||
|
<rect x="14" y="3" width="7" height="7" rx="1" />
|
||||||
|
<rect x="3" y="14" width="7" height="7" rx="1" />
|
||||||
|
<rect x="14" y="14" width="7" height="7" rx="1" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/warehouse/suppliers",
|
||||||
|
label: "Dodavatelé",
|
||||||
|
permission: "warehouse.manage",
|
||||||
|
matchPrefix: "/warehouse/suppliers",
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||||
|
<circle cx="9" cy="7" r="4" />
|
||||||
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
||||||
|
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import useModalLock from "../../hooks/useModalLock";
|
|||||||
import apiFetch from "../../utils/api";
|
import apiFetch from "../../utils/api";
|
||||||
import { formatSessionDate } from "../../utils/dashboardHelpers";
|
import { formatSessionDate } from "../../utils/dashboardHelpers";
|
||||||
import { sessionsOptions, type Session } from "../../lib/queries/dashboard";
|
import { sessionsOptions, type Session } from "../../lib/queries/dashboard";
|
||||||
import { Skeleton } from "boneyard-js/react";
|
|
||||||
import DashSessionsFixture from "../../fixtures/DashSessionsFixture";
|
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
@@ -154,11 +152,11 @@ export default function DashSessions() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-card-body" style={{ padding: 0 }}>
|
<div className="admin-card-body" style={{ padding: 0 }}>
|
||||||
<Skeleton
|
{sessionsLoading ? (
|
||||||
name="dash-sessions"
|
<div className="admin-loading">
|
||||||
loading={sessionsLoading}
|
<div className="admin-spinner" />
|
||||||
fixture={<DashSessionsFixture />}
|
</div>
|
||||||
>
|
) : (
|
||||||
<>
|
<>
|
||||||
{sessions.length === 0 && (
|
{sessions.length === 0 && (
|
||||||
<div
|
<div
|
||||||
@@ -231,7 +229,7 @@ export default function DashSessions() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
</Skeleton>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,13 @@ interface Batch {
|
|||||||
is_consumed: boolean;
|
is_consumed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BatchesResponse {
|
||||||
|
batches: Batch[];
|
||||||
|
total_stock: number;
|
||||||
|
reserved_quantity: number;
|
||||||
|
available_quantity: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface BatchPickerProps {
|
interface BatchPickerProps {
|
||||||
itemId: number | null;
|
itemId: number | null;
|
||||||
value: number | null;
|
value: number | null;
|
||||||
@@ -20,29 +27,48 @@ export default function BatchPicker({
|
|||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
}: BatchPickerProps) {
|
}: BatchPickerProps) {
|
||||||
const { data: batches } = useQuery({
|
const { data: response } = useQuery({
|
||||||
queryKey: ["warehouse", "batches", itemId],
|
queryKey: ["warehouse", "batches", itemId],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
jsonQuery<Batch[]>(`/api/admin/warehouse/items/${itemId}/batches`),
|
jsonQuery<BatchesResponse>(
|
||||||
|
`/api/admin/warehouse/items/${itemId}/batches`,
|
||||||
|
),
|
||||||
enabled: !!itemId,
|
enabled: !!itemId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const batches = response?.batches ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div>
|
||||||
|
{response && response.reserved_quantity > 0 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
color: "var(--text-tertiary)",
|
||||||
|
marginBottom: "0.25rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Dostupné: {response.available_quantity} ks (z toho rezervováno:{" "}
|
||||||
|
{response.reserved_quantity} ks)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<select
|
<select
|
||||||
className="admin-form-select"
|
className="admin-form-select"
|
||||||
value={value ?? ""}
|
value={value ?? ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const batchId = Number(e.target.value);
|
const batchId = Number(e.target.value);
|
||||||
const batch = batches?.find((b) => b.id === batchId);
|
const batch = batches.find((b) => b.id === batchId);
|
||||||
if (batch) onChange(batch.id, Number(batch.unit_price));
|
if (batch) onChange(batch.id, Number(batch.unit_price));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">-- auto FIFO --</option>
|
<option value="">-- auto FIFO --</option>
|
||||||
{batches?.map((b) => (
|
{batches.map((b) => (
|
||||||
<option key={b.id} value={b.id}>
|
<option key={b.id} value={b.id}>
|
||||||
{new Date(b.received_at).toLocaleDateString("cs-CZ")} | {b.quantity}{" "}
|
{new Date(b.received_at).toLocaleDateString("cs-CZ")} | {b.quantity}{" "}
|
||||||
ks | {Number(b.unit_price).toFixed(2)} Kč
|
ks | {Number(b.unit_price).toFixed(2)} Kč
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,210 @@
|
|||||||
import { useState } from "react";
|
import { useState, useRef, useEffect, useCallback, useId } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { warehouseItemListOptions } from "../../lib/queries/warehouse";
|
import { warehouseItemListOptions } from "../../lib/queries/warehouse";
|
||||||
|
|
||||||
interface ItemPickerProps {
|
interface ItemPickerProps {
|
||||||
value: number | null;
|
value: number | null;
|
||||||
onChange: (itemId: number) => void;
|
onChange: (itemId: number) => void;
|
||||||
|
itemName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ItemPicker({ value, onChange }: ItemPickerProps) {
|
export default function ItemPicker({
|
||||||
const [search, setSearch] = useState("");
|
value,
|
||||||
|
onChange,
|
||||||
|
itemName,
|
||||||
|
}: ItemPickerProps) {
|
||||||
|
const [search, setSearch] = useState(itemName ?? "");
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const { data } = useQuery(warehouseItemListOptions({ search, perPage: 20 }));
|
const { data } = useQuery(warehouseItemListOptions({ search, perPage: 20 }));
|
||||||
const items = data?.data ?? [];
|
const items = data?.data ?? [];
|
||||||
|
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const activeIndexRef = useRef<number>(-1);
|
||||||
|
const listboxId = useId();
|
||||||
|
const [activeIndex, setActiveIndex] = useState<number>(-1);
|
||||||
|
|
||||||
|
const [dropdownStyle, setDropdownStyle] = useState<{
|
||||||
|
position: "fixed";
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
width: number;
|
||||||
|
zIndex: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const updatePosition = useCallback(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
|
setDropdownStyle({
|
||||||
|
position: "fixed",
|
||||||
|
top: rect.bottom + 2,
|
||||||
|
left: rect.left,
|
||||||
|
width: rect.width,
|
||||||
|
zIndex: 100,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Reset active index when items change
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeIndex >= items.length) {
|
||||||
|
activeIndexRef.current = -1;
|
||||||
|
setActiveIndex(-1);
|
||||||
|
}
|
||||||
|
}, [items, activeIndex]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
updatePosition();
|
||||||
|
const onScroll = () => updatePosition();
|
||||||
|
const onClose = () => setOpen(false);
|
||||||
|
window.addEventListener("scroll", onScroll, true);
|
||||||
|
window.addEventListener("resize", onClose);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("scroll", onScroll, true);
|
||||||
|
window.removeEventListener("resize", onClose);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
setDropdownStyle(null);
|
||||||
|
activeIndexRef.current = -1;
|
||||||
|
setActiveIndex(-1);
|
||||||
|
}
|
||||||
|
}, [open, updatePosition]);
|
||||||
|
|
||||||
|
// Close on click-outside via mousedown on document
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
const onDocMouseDown = (e: MouseEvent) => {
|
||||||
|
const target = e.target;
|
||||||
|
if (containerRef.current && target instanceof Node) {
|
||||||
|
if (!containerRef.current.contains(target)) {
|
||||||
|
// Don't close if click landed on an option in the portal
|
||||||
|
const listEl = document.getElementById(listboxId);
|
||||||
|
if (listEl && listEl.contains(target)) return;
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", onDocMouseDown);
|
||||||
|
return () => document.removeEventListener("mousedown", onDocMouseDown);
|
||||||
|
}, [open, listboxId]);
|
||||||
|
|
||||||
|
const handleSelect = (itemId: number, name: string) => {
|
||||||
|
onChange(itemId);
|
||||||
|
setOpen(false);
|
||||||
|
setSearch(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setActive = (index: number) => {
|
||||||
|
activeIndexRef.current = index;
|
||||||
|
setActiveIndex(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === "ArrowDown") {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!open) {
|
||||||
|
setOpen(true);
|
||||||
|
if (items.length > 0) setActive(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const next =
|
||||||
|
activeIndexRef.current < items.length - 1
|
||||||
|
? activeIndexRef.current + 1
|
||||||
|
: 0;
|
||||||
|
setActive(next);
|
||||||
|
} else if (e.key === "ArrowUp") {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!open) {
|
||||||
|
setOpen(true);
|
||||||
|
if (items.length > 0) setActive(items.length - 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const prev =
|
||||||
|
activeIndexRef.current > 0
|
||||||
|
? activeIndexRef.current - 1
|
||||||
|
: items.length - 1;
|
||||||
|
setActive(prev);
|
||||||
|
} else if (e.key === "Enter") {
|
||||||
|
if (
|
||||||
|
open &&
|
||||||
|
activeIndexRef.current >= 0 &&
|
||||||
|
activeIndexRef.current < items.length
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
const item = items[activeIndexRef.current];
|
||||||
|
handleSelect(item.id, item.name);
|
||||||
|
}
|
||||||
|
} else if (e.key === "Escape") {
|
||||||
|
if (open) {
|
||||||
|
e.preventDefault();
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
} else if (e.key === "Home") {
|
||||||
|
if (open && items.length > 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
setActive(0);
|
||||||
|
}
|
||||||
|
} else if (e.key === "End") {
|
||||||
|
if (open && items.length > 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
setActive(items.length - 1);
|
||||||
|
}
|
||||||
|
} else if (e.key === "Tab") {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeId =
|
||||||
|
activeIndex >= 0 ? `${listboxId}-option-${activeIndex}` : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="admin-form-group">
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
style={{ position: "relative" }}
|
||||||
|
role="combobox"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-owns={listboxId}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
placeholder="Hledat položku..."
|
placeholder="Hledat položku..."
|
||||||
value={search}
|
value={search}
|
||||||
|
role="searchbox"
|
||||||
|
aria-autocomplete="list"
|
||||||
|
aria-controls={listboxId}
|
||||||
|
aria-activedescendant={activeId}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSearch(e.target.value);
|
setSearch(e.target.value);
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
}}
|
}}
|
||||||
onFocus={() => setOpen(true)}
|
onFocus={() => {
|
||||||
onBlur={() => setTimeout(() => setOpen(false), 200)}
|
setOpen(true);
|
||||||
|
updatePosition();
|
||||||
|
}}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
/>
|
/>
|
||||||
{open && items.length > 0 && (
|
{open &&
|
||||||
<ul className="admin-item-picker-list">
|
items.length > 0 &&
|
||||||
{items.map((item) => (
|
dropdownStyle &&
|
||||||
|
createPortal(
|
||||||
|
<ul
|
||||||
|
id={listboxId}
|
||||||
|
role="listbox"
|
||||||
|
className="admin-item-picker-list"
|
||||||
|
style={dropdownStyle}
|
||||||
|
>
|
||||||
|
{items.map((item, index) => (
|
||||||
<li
|
<li
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className={`admin-item-picker-item ${value === item.id ? "active" : ""}`}
|
id={`${listboxId}-option-${index}`}
|
||||||
onClick={() => {
|
role="option"
|
||||||
onChange(item.id);
|
aria-selected={value === item.id}
|
||||||
setOpen(false);
|
className={`admin-item-picker-item ${activeIndex === index ? "active" : ""}`}
|
||||||
setSearch(item.name);
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSelect(item.id, item.name);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="admin-item-picker-name">{item.name}</span>
|
<span className="admin-item-picker-name">{item.name}</span>
|
||||||
@@ -52,7 +220,8 @@ export default function ItemPicker({ value, onChange }: ItemPickerProps) {
|
|||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>,
|
||||||
|
document.body,
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
54
src/admin/components/warehouse/ReservationPicker.tsx
Normal file
54
src/admin/components/warehouse/ReservationPicker.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { warehouseReservationListOptions } from "../../lib/queries/warehouse";
|
||||||
|
|
||||||
|
interface ReservationPickerProps {
|
||||||
|
itemId: number | null;
|
||||||
|
projectId: number | null;
|
||||||
|
value: number | null;
|
||||||
|
onChange: (reservationId: number | null, remainingQty: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ReservationPicker({
|
||||||
|
itemId,
|
||||||
|
projectId,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: ReservationPickerProps) {
|
||||||
|
const { data: result } = useQuery(
|
||||||
|
warehouseReservationListOptions({
|
||||||
|
item_id: itemId ?? undefined,
|
||||||
|
project_id: projectId ?? undefined,
|
||||||
|
status: "ACTIVE",
|
||||||
|
perPage: 100,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const reservations = result?.data ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
className="admin-form-select"
|
||||||
|
value={value ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
if (!val) {
|
||||||
|
onChange(null, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const reservationId = Number(val);
|
||||||
|
const reservation = reservations.find((r) => r.id === reservationId);
|
||||||
|
if (reservation) {
|
||||||
|
onChange(reservationId, Number(reservation.remaining_qty));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">— žádná rezervace —</option>
|
||||||
|
{reservations.map((r) => (
|
||||||
|
<option key={r.id} value={r.id}>
|
||||||
|
R{r.id} — {r.project?.name ?? `Projekt #${r.project_id}`} — zbývá:{" "}
|
||||||
|
{Number(r.remaining_qty)} {r.item?.unit ?? "ks"}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { WarehouseLocation } from "../../lib/queries/warehouse";
|
import { ReactNode, useId, useRef } from "react";
|
||||||
import ItemPicker from "./ItemPicker";
|
import ItemPicker from "./ItemPicker";
|
||||||
import LocationSelect from "./LocationSelect";
|
import LocationSelect from "./LocationSelect";
|
||||||
import BatchPicker from "./BatchPicker";
|
import BatchPicker from "./BatchPicker";
|
||||||
|
|
||||||
export interface MovementLine {
|
export interface MovementItem {
|
||||||
key: string;
|
key: string;
|
||||||
item_id: number | null;
|
item_id: number | null;
|
||||||
item_name?: string;
|
item_name?: string;
|
||||||
@@ -15,25 +15,30 @@ export interface MovementLine {
|
|||||||
notes: string | null;
|
notes: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WarehouseMovementTableProps {
|
export type MovementRowRenderer<T> = (
|
||||||
lines: MovementLine[];
|
item: T,
|
||||||
onChange: (lines: MovementLine[]) => void;
|
index: number,
|
||||||
mode: "receipt" | "issue";
|
updateItem: (field: string, value: unknown) => void,
|
||||||
locations: WarehouseLocation[];
|
removeItem: () => void,
|
||||||
|
) => ReactNode;
|
||||||
|
|
||||||
|
interface WarehouseMovementTableProps<T extends { key: string }> {
|
||||||
|
items: T[];
|
||||||
|
onChange: (items: T[]) => void;
|
||||||
|
mode: "receipt" | "issue" | "inventory";
|
||||||
|
defaultItem?: () => Omit<T, "key">;
|
||||||
|
renderRow?: MovementRowRenderer<T>;
|
||||||
|
renderHeader?: () => ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
let lineCounter = 0;
|
function parseDecimal(raw: string): number {
|
||||||
|
// Strip leading minus if you want negative support; here we want only positive
|
||||||
|
const cleaned = raw.replace(/[eE+\-]/g, "");
|
||||||
|
const n = Number(cleaned);
|
||||||
|
return Number.isFinite(n) ? n : 0;
|
||||||
|
}
|
||||||
|
|
||||||
export default function WarehouseMovementTable({
|
const builtInDefaultMovementItem = (): Omit<MovementItem, "key"> => ({
|
||||||
lines,
|
|
||||||
onChange,
|
|
||||||
mode,
|
|
||||||
}: WarehouseMovementTableProps) {
|
|
||||||
const addLine = () => {
|
|
||||||
onChange([
|
|
||||||
...lines,
|
|
||||||
{
|
|
||||||
key: `line-${++lineCounter}`,
|
|
||||||
item_id: null,
|
item_id: null,
|
||||||
quantity: 0,
|
quantity: 0,
|
||||||
unit_price: 0,
|
unit_price: 0,
|
||||||
@@ -41,106 +46,202 @@ export default function WarehouseMovementTable({
|
|||||||
batch_id: null,
|
batch_id: null,
|
||||||
reservation_id: null,
|
reservation_id: null,
|
||||||
notes: null,
|
notes: null,
|
||||||
},
|
});
|
||||||
]);
|
|
||||||
|
export default function WarehouseMovementTable<T extends { key: string }>({
|
||||||
|
items,
|
||||||
|
onChange,
|
||||||
|
mode,
|
||||||
|
defaultItem,
|
||||||
|
renderRow,
|
||||||
|
renderHeader,
|
||||||
|
}: WarehouseMovementTableProps<T>) {
|
||||||
|
const baseId = useId();
|
||||||
|
const counterRef = useRef(0);
|
||||||
|
const nextKey = () => `${baseId}-item-${counterRef.current++}`;
|
||||||
|
|
||||||
|
const addItem = () => {
|
||||||
|
const factory =
|
||||||
|
defaultItem ??
|
||||||
|
(builtInDefaultMovementItem as unknown as () => Omit<T, "key">);
|
||||||
|
onChange([...items, { ...(factory() as object), key: nextKey() } as T]);
|
||||||
};
|
};
|
||||||
const removeLine = (index: number) => {
|
const removeItem = (index: number) => {
|
||||||
onChange(lines.filter((_, i) => i !== index));
|
onChange(items.filter((_, i) => i !== index));
|
||||||
};
|
};
|
||||||
const updateLine = (index: number, field: string, value: unknown) => {
|
const updateItem = (index: number, field: string, value: unknown) => {
|
||||||
const updated = [...lines];
|
const updated = [...items];
|
||||||
updated[index] = { ...updated[index], [field]: value };
|
updated[index] = { ...updated[index], [field]: value };
|
||||||
onChange(updated);
|
onChange(updated);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If a custom row renderer is supplied, the caller is fully in charge of
|
||||||
|
// the row layout (and typically the column headers too — see inventory mode).
|
||||||
|
if (renderRow) {
|
||||||
return (
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="admin-warehouse-movement-table">
|
||||||
|
<table className="admin-table">
|
||||||
|
{renderHeader && (
|
||||||
|
<thead>
|
||||||
|
<tr>{renderHeader()}</tr>
|
||||||
|
</thead>
|
||||||
|
)}
|
||||||
|
<tbody>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<tr key={item.key}>
|
||||||
|
{renderRow(
|
||||||
|
item,
|
||||||
|
index,
|
||||||
|
(field, value) => updateItem(index, field, value),
|
||||||
|
() => removeItem(index),
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="admin-btn admin-btn-secondary"
|
||||||
|
onClick={addItem}
|
||||||
|
>
|
||||||
|
+ Přidat řádek
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default rendering: only valid for "receipt" | "issue" — narrow at runtime.
|
||||||
|
const movementItems = items as unknown as MovementItem[];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
<div className="admin-warehouse-movement-table">
|
<div className="admin-warehouse-movement-table">
|
||||||
<table className="admin-table">
|
<table className="admin-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Položka</th>
|
<th className="admin-warehouse-col-item">Položka</th>
|
||||||
{mode === "issue" && <th>Šarže</th>}
|
{mode === "issue" && (
|
||||||
<th>Množství</th>
|
<th className="admin-warehouse-col-batch">Šarže</th>
|
||||||
<th>Cena/ks</th>
|
)}
|
||||||
<th>Lokace</th>
|
<th className="admin-warehouse-col-qty">Množství</th>
|
||||||
<th>Poznámka</th>
|
<th className="admin-warehouse-col-price">Cena/ks</th>
|
||||||
<th></th>
|
<th className="admin-warehouse-col-location">Lokace</th>
|
||||||
|
<th className="admin-warehouse-col-notes">Poznámka</th>
|
||||||
|
<th>Akce</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{lines.map((line, index) => (
|
{movementItems.map((item, index) => (
|
||||||
<tr key={line.key}>
|
<tr key={item.key}>
|
||||||
<td>
|
<td className="admin-warehouse-col-item">
|
||||||
<ItemPicker
|
<ItemPicker
|
||||||
value={line.item_id}
|
value={item.item_id}
|
||||||
onChange={(id) => updateLine(index, "item_id", id)}
|
itemName={item.item_name}
|
||||||
|
onChange={(id) => updateItem(index, "item_id", id)}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
{mode === "issue" && (
|
{mode === "issue" && (
|
||||||
<td>
|
<td className="admin-warehouse-col-batch">
|
||||||
<BatchPicker
|
<BatchPicker
|
||||||
itemId={line.item_id}
|
itemId={item.item_id}
|
||||||
value={line.batch_id}
|
value={item.batch_id}
|
||||||
onChange={(batchId, unitPrice) => {
|
onChange={(batchId, unitPrice) => {
|
||||||
updateLine(index, "batch_id", batchId);
|
updateItem(index, "batch_id", batchId);
|
||||||
updateLine(index, "unit_price", unitPrice);
|
updateItem(index, "unit_price", unitPrice);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
<td>
|
<td className="admin-warehouse-col-qty">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
value={line.quantity || ""}
|
value={item.quantity || ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateLine(index, "quantity", Number(e.target.value))
|
updateItem(
|
||||||
|
index,
|
||||||
|
"quantity",
|
||||||
|
parseDecimal(e.target.value),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
min="0"
|
min="0"
|
||||||
step="0.001"
|
step="0.001"
|
||||||
|
inputMode="decimal"
|
||||||
|
pattern="[0-9]+([\.,][0-9]+)?"
|
||||||
|
aria-label={`Množství, řádek ${index + 1}`}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td className="admin-warehouse-col-price">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
value={line.unit_price || ""}
|
value={item.unit_price || ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateLine(index, "unit_price", Number(e.target.value))
|
updateItem(
|
||||||
|
index,
|
||||||
|
"unit_price",
|
||||||
|
parseDecimal(e.target.value),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
min="0"
|
min="0"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
disabled={mode === "issue"}
|
disabled={mode === "issue"}
|
||||||
|
inputMode="decimal"
|
||||||
|
pattern="[0-9]+([\.,][0-9]+)?"
|
||||||
|
aria-label={`Cena za kus, řádek ${index + 1}`}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td className="admin-warehouse-col-location">
|
||||||
<LocationSelect
|
<LocationSelect
|
||||||
value={line.location_id}
|
value={item.location_id}
|
||||||
onChange={(id) => updateLine(index, "location_id", id)}
|
onChange={(id) => updateItem(index, "location_id", id)}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td className="admin-warehouse-col-notes">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
value={line.notes ?? ""}
|
value={item.notes ?? ""}
|
||||||
onChange={(e) => updateLine(index, "notes", e.target.value)}
|
onChange={(e) => updateItem(index, "notes", e.target.value)}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="admin-btn-danger-sm"
|
className="admin-btn-icon danger"
|
||||||
onClick={() => removeLine(index)}
|
onClick={() => removeItem(index)}
|
||||||
|
title="Odebrat řádek"
|
||||||
|
aria-label="Odebrat řádek"
|
||||||
>
|
>
|
||||||
✕
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<button type="button" className="admin-btn-secondary" onClick={addLine}>
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="admin-btn admin-btn-secondary"
|
||||||
|
onClick={addItem}
|
||||||
|
>
|
||||||
+ Přidat řádek
|
+ Přidat řádek
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
export default function AttendanceAdminFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Správa docházky</h1>
|
|
||||||
</div>
|
|
||||||
<div className="admin-page-actions">
|
|
||||||
<button className="admin-btn admin-btn-secondary">
|
|
||||||
Vyplnit měsíc
|
|
||||||
</button>
|
|
||||||
<button className="admin-btn admin-btn-primary">Přidat záznam</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card mb-6">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-form-row">
|
|
||||||
<label className="admin-form-label">Měsíc</label>
|
|
||||||
<select className="admin-form-select" />
|
|
||||||
<label className="admin-form-label">Zaměstnanec</label>
|
|
||||||
<select className="admin-form-select" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-grid admin-grid-3">
|
|
||||||
{Array.from({ length: 3 }, (_, i) => (
|
|
||||||
<div key={i} className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="flex-row gap-2 mb-2">
|
|
||||||
<span style={{ fontWeight: 600 }}>Jan Novák</span>
|
|
||||||
<span className="attendance-working-badge finished">
|
|
||||||
✗
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="admin-stat-value">8:00</div>
|
|
||||||
<div className="admin-stat-label">odpracováno</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th>Příchod</th>
|
|
||||||
<th>Odchod</th>
|
|
||||||
<th>Hodiny</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 4 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>1. 4. 2025</td>
|
|
||||||
<td className="admin-mono">08:00</td>
|
|
||||||
<td className="admin-mono">16:00</td>
|
|
||||||
<td className="admin-mono">8:00</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
export default function AttendanceBalancesFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Správa bilancí</h1>
|
|
||||||
</div>
|
|
||||||
<div className="admin-page-actions">
|
|
||||||
<select className="admin-form-select" style={{ minWidth: 100 }}>
|
|
||||||
<option>2025</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Zaměstnanec</th>
|
|
||||||
<th>Nárok (h)</th>
|
|
||||||
<th>Čerpáno (h)</th>
|
|
||||||
<th>Zbývá (h)</th>
|
|
||||||
<th>Nemoc (h)</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 4 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="fw-500">Jan Novák</td>
|
|
||||||
<td className="admin-mono">160</td>
|
|
||||||
<td className="admin-mono">40.0</td>
|
|
||||||
<td className="admin-mono">120.0</td>
|
|
||||||
<td className="admin-mono">8.0</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn-icon" title="Upravit">
|
|
||||||
✎
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-6">
|
|
||||||
<h2 className="admin-page-title mb-4" style={{ fontSize: "1.25rem" }}>
|
|
||||||
Měsíční přehled fondu 2025
|
|
||||||
</h2>
|
|
||||||
<div className="admin-grid admin-grid-3">
|
|
||||||
{Array.from({ length: 3 }, (_, i) => (
|
|
||||||
<div key={i} className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<h3 style={{ fontWeight: 600, fontSize: "1rem", margin: 0 }}>
|
|
||||||
Duben
|
|
||||||
</h3>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "0.375rem",
|
|
||||||
marginTop: "0.5rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
fontSize: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>Jan Novák</span>
|
|
||||||
<span className="text-secondary">8h</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
height: 3,
|
|
||||||
background: "var(--bg-tertiary)",
|
|
||||||
borderRadius: 2,
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
height: "100%",
|
|
||||||
width: "75%",
|
|
||||||
background: "var(--gradient)",
|
|
||||||
borderRadius: 2,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
export default function AttendanceCreateFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<h1 className="admin-page-title">Zapsat docházku</h1>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card" style={{ maxWidth: 600 }}>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<FormField label="Uživatel">
|
|
||||||
<select className="admin-form-select">
|
|
||||||
<option>Jan Novák</option>
|
|
||||||
</select>
|
|
||||||
</FormField>
|
|
||||||
<FormField label="Datum">
|
|
||||||
<input type="date" className="admin-form-input" />
|
|
||||||
</FormField>
|
|
||||||
<FormField label="Příchod">
|
|
||||||
<input type="time" className="admin-form-input" />
|
|
||||||
</FormField>
|
|
||||||
<FormField label="Odchod">
|
|
||||||
<input type="time" className="admin-form-input" />
|
|
||||||
</FormField>
|
|
||||||
<button className="admin-btn admin-btn-primary">Uložit</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FormField({
|
|
||||||
label,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">{label}</label>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
export default function AttendanceFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Docházka</h1>
|
|
||||||
<p className="admin-page-subtitle">pondělí 28. dubna 2025</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="attendance-layout">
|
|
||||||
<div className="attendance-main">
|
|
||||||
<div className="attendance-clock-card">
|
|
||||||
<div className="attendance-clock-header">
|
|
||||||
<div className="attendance-clock-status">
|
|
||||||
<span className="attendance-status-dot" />
|
|
||||||
<span>Nepracuji</span>
|
|
||||||
</div>
|
|
||||||
<div className="attendance-clock-time">08:30</div>
|
|
||||||
</div>
|
|
||||||
<div className="attendance-clock-actions">
|
|
||||||
<button className="admin-btn admin-btn-primary w-full">
|
|
||||||
Příchod
|
|
||||||
</button>
|
|
||||||
<button className="admin-btn admin-btn-secondary w-full">
|
|
||||||
Žádost o nepřítomnost
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="attendance-sidebar">
|
|
||||||
<div className="attendance-balance-card">
|
|
||||||
<h3 className="attendance-balance-title">Dovolená 2025</h3>
|
|
||||||
<div className="attendance-balance-value">
|
|
||||||
<span className="attendance-balance-number">12</span>
|
|
||||||
<span className="attendance-balance-unit">dnů</span>
|
|
||||||
</div>
|
|
||||||
<div className="attendance-balance-detail">
|
|
||||||
<span>Celkem: 160h</span>
|
|
||||||
<span>Čerpáno: 64h</span>
|
|
||||||
</div>
|
|
||||||
<div className="attendance-balance-bar">
|
|
||||||
<div
|
|
||||||
className="attendance-balance-progress"
|
|
||||||
style={{ width: "60%" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-stat-card">
|
|
||||||
<div className="admin-stat-icon danger">
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="admin-stat-content">
|
|
||||||
<span className="admin-stat-label">Nemoc 2025</span>
|
|
||||||
<span className="admin-stat-value">16h čerpáno</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="attendance-quick-links">
|
|
||||||
<h4 className="attendance-quick-title">Rychlé odkazy</h4>
|
|
||||||
<a className="attendance-quick-link">
|
|
||||||
<span>Moje žádosti</span>
|
|
||||||
</a>
|
|
||||||
<a className="attendance-quick-link">
|
|
||||||
<span>Historie docházky</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
export default function AttendanceHistoryFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Historie docházky</h1>
|
|
||||||
<p className="admin-page-subtitle">duben 2025</p>
|
|
||||||
</div>
|
|
||||||
<div className="admin-page-actions">
|
|
||||||
<button className="admin-btn admin-btn-secondary">Tisk</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card mb-6">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-form-row">
|
|
||||||
<label className="admin-form-label">Měsíc</label>
|
|
||||||
<input className="admin-form-input" readOnly value="04/2025" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card mb-6">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
|
|
||||||
<div className="admin-stat-icon info">
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
|
|
||||||
<line x1="16" y1="2" x2="16" y2="6" />
|
|
||||||
<line x1="8" y1="2" x2="8" y2="6" />
|
|
||||||
<line x1="3" y1="10" x2="21" y2="10" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div style={{ flex: 1, minWidth: 200 }}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "baseline",
|
|
||||||
marginBottom: "0.375rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span style={{ fontWeight: 600 }}>Fond: 120h / 160h</span>
|
|
||||||
<span
|
|
||||||
className="text-secondary"
|
|
||||||
style={{ fontSize: "0.8125rem" }}
|
|
||||||
>
|
|
||||||
20 prac. dnů
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="attendance-balance-bar">
|
|
||||||
<div
|
|
||||||
className="attendance-balance-progress"
|
|
||||||
style={{ width: "75%" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th>Typ</th>
|
|
||||||
<th>Příchod</th>
|
|
||||||
<th>Pauza</th>
|
|
||||||
<th>Odchod</th>
|
|
||||||
<th>Hodiny</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="admin-mono">1. 4. 2025</td>
|
|
||||||
<td>
|
|
||||||
<span className="admin-badge admin-badge-info">
|
|
||||||
Práce
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="admin-mono">08:00</td>
|
|
||||||
<td className="admin-mono">12:00 – 12:30</td>
|
|
||||||
<td className="admin-mono">16:30</td>
|
|
||||||
<td className="admin-mono">8:00</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
export default function AttendanceLocationFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}>
|
|
||||||
<svg
|
|
||||||
width="32"
|
|
||||||
height="32"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
|
|
||||||
<circle cx="12" cy="10" r="3" />
|
|
||||||
</svg>
|
|
||||||
<h1 className="admin-page-title">Lokace</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card" style={{ height: 300, marginBottom: "1rem" }}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
background: "var(--bg-secondary)",
|
|
||||||
height: "100%",
|
|
||||||
borderRadius: 8,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem" }}
|
|
||||||
>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<h3 style={{ marginBottom: "0.5rem" }}>Poloha</h3>
|
|
||||||
<p>50.0755° N, 14.4378° E</p>
|
|
||||||
<p>Praha, Česká republika</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<h3 style={{ marginBottom: "0.5rem" }}>Čas záznamu</h3>
|
|
||||||
<p>1. 1. 2024 08:00</p>
|
|
||||||
<p>Přesnost: 10 m</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
export default function AuditLogFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Audit log</h1>
|
|
||||||
<p className="admin-page-subtitle">Záznam změn v systému</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div
|
|
||||||
className="admin-search-bar mb-4"
|
|
||||||
style={{ display: "flex", gap: "0.5rem" }}
|
|
||||||
>
|
|
||||||
<input className="admin-form-input" placeholder="" />
|
|
||||||
<select className="admin-form-select" />
|
|
||||||
<select className="admin-form-select" />
|
|
||||||
</div>
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Čas</th>
|
|
||||||
<th>Uživatel</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
<th>Entita</th>
|
|
||||||
<th>Detail</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="admin-mono">1. 1. 2024 10:00</td>
|
|
||||||
<td>admin</td>
|
|
||||||
<td>
|
|
||||||
<span className="admin-badge admin-badge-create">
|
|
||||||
Vytvoření
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>Faktura</td>
|
|
||||||
<td style={{ maxWidth: 300 }}>Nová faktura FV-2024-001</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
export default function CompanySettingsFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Nastavení firmy</h1>
|
|
||||||
<p className="admin-page-subtitle">Firemní údaje a bankovní účty</p>
|
|
||||||
</div>
|
|
||||||
<button className="admin-btn admin-btn-primary">
|
|
||||||
Uložit nastavení
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="admin-settings-grid">
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-header">
|
|
||||||
<h3 className="admin-card-title">Firemní údaje</h3>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-form">
|
|
||||||
<label className="admin-form-label">Název firmy</label>
|
|
||||||
<input
|
|
||||||
className="admin-form-input"
|
|
||||||
readOnly
|
|
||||||
value="BOHA s.r.o."
|
|
||||||
/>
|
|
||||||
<div className="admin-form-row">
|
|
||||||
<label className="admin-form-label">Ulice</label>
|
|
||||||
<input
|
|
||||||
className="admin-form-input"
|
|
||||||
readOnly
|
|
||||||
value="Hlavní 123"
|
|
||||||
/>
|
|
||||||
<label className="admin-form-label">Město</label>
|
|
||||||
<input className="admin-form-input" readOnly value="Praha" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-header">
|
|
||||||
<h3 className="admin-card-title">Bankovní účty</h3>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Název</th>
|
|
||||||
<th>Banka</th>
|
|
||||||
<th>Číslo účtu</th>
|
|
||||||
<th>Měna</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Hlavní účet</td>
|
|
||||||
<td>ČSOB</td>
|
|
||||||
<td className="admin-mono">123456/0300</td>
|
|
||||||
<td>CZK</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
export default function DashSessionsFixture() {
|
|
||||||
return (
|
|
||||||
<div className="admin-card">
|
|
||||||
<div
|
|
||||||
className="admin-card-header"
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
gap: "0.75rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h2 className="admin-card-title">Přihlášená zařízení</h2>
|
|
||||||
<button className="admin-btn admin-btn-secondary admin-btn-sm">
|
|
||||||
Odhlásit ostatní
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card-body" style={{ padding: 0 }}>
|
|
||||||
<div className="dash-sessions-list">
|
|
||||||
{Array.from({ length: 3 }, (_, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className={`dash-session-item${i === 0 ? " dash-session-item-current" : ""}`}
|
|
||||||
>
|
|
||||||
<div className="dash-session-icon">
|
|
||||||
<svg
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
|
|
||||||
<line x1="8" y1="21" x2="16" y2="21" />
|
|
||||||
<line x1="12" y1="17" x2="12" y2="21" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="dash-session-info">
|
|
||||||
<div className="dash-session-device">
|
|
||||||
Chrome na Windows
|
|
||||||
{i === 0 && (
|
|
||||||
<span
|
|
||||||
className="admin-badge admin-badge-success"
|
|
||||||
style={{ marginLeft: "0.5rem" }}
|
|
||||||
>
|
|
||||||
Aktuální
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="dash-session-meta">
|
|
||||||
<span>192.168.1.100</span>
|
|
||||||
<span className="dash-session-meta-separator">|</span>
|
|
||||||
<span>před 2 hodinami</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
export default function DashboardFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Dashboard</h1>
|
|
||||||
<p className="admin-page-subtitle">Přehled</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="dash-kpi-grid"
|
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "repeat(4, 1fr)",
|
|
||||||
gap: "1rem",
|
|
||||||
marginBottom: "1rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{["Nabídky", "Objednávky", "Faktury", "Projekty"].map((label, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="dash-kpi-card"
|
|
||||||
style={{
|
|
||||||
padding: "1.25rem",
|
|
||||||
borderRadius: 10,
|
|
||||||
background: "var(--bg-secondary)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ fontSize: "0.875rem", marginBottom: "0.25rem" }}>
|
|
||||||
{label}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: "1.5rem", fontWeight: 600 }}>12</div>
|
|
||||||
<div style={{ fontSize: "0.75rem" }}>tento měsíc</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="dash-quick-actions"
|
|
||||||
style={{ display: "flex", gap: "0.5rem", marginBottom: "1rem" }}
|
|
||||||
>
|
|
||||||
{["Nová nabídka", "Nová faktura", "Zapsat docházku"].map((label, i) => (
|
|
||||||
<button
|
|
||||||
key={i}
|
|
||||||
className="admin-btn admin-btn-secondary"
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "2fr 1fr",
|
|
||||||
gap: "1rem",
|
|
||||||
marginBottom: "1rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="admin-card" style={{ minHeight: 320 }}>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<h3 className="admin-card-title">Docházka dnes</h3>
|
|
||||||
<div style={{ height: 200 }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
||||||
<div className="admin-card" style={{ minHeight: 150 }}>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<h3 className="admin-card-title">Aktivita</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card" style={{ minHeight: 150 }}>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<h3 className="admin-card-title">Profil</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem" }}
|
|
||||||
>
|
|
||||||
<div className="admin-card" style={{ minHeight: 200 }}>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<h3 className="admin-card-title">Relace</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card" style={{ minHeight: 200 }}>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<h3 className="admin-card-title">Poslední aktivity</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
export default function InvoiceDetailFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
|
||||||
<a href="/invoices" className="admin-btn-icon">
|
|
||||||
<svg
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<path d="M19 12H5M12 19l-7-7 7-7" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">FV-2024-001</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-page-actions">
|
|
||||||
<button className="admin-btn admin-btn-primary">Uložit</button>
|
|
||||||
<button className="admin-btn admin-btn-secondary">Zaplaceno</button>
|
|
||||||
<button className="admin-btn admin-btn-secondary">Smazat</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card" style={{ marginBottom: "1rem" }}>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div
|
|
||||||
className="admin-form-row"
|
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "1fr 1fr",
|
|
||||||
gap: "1rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Zákazník</label>
|
|
||||||
<div>Firma s.r.o.</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Stav</label>
|
|
||||||
<span className="admin-badge admin-badge-invoice-issued">
|
|
||||||
Vystavena
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Datum vystavení</label>
|
|
||||||
<div>1. 1. 2024</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Datum splatnosti</label>
|
|
||||||
<div>15. 1. 2024</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<h3 className="admin-card-title">Položky</h3>
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Položka</th>
|
|
||||||
<th>Množství</th>
|
|
||||||
<th>Cena</th>
|
|
||||||
<th>Celkem</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 3 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>Služba {i + 1}</td>
|
|
||||||
<td>1</td>
|
|
||||||
<td>10 000 Kč</td>
|
|
||||||
<td>10 000 Kč</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
export default function InvoicesFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Faktury</h1>
|
|
||||||
<p className="admin-page-subtitle">15 faktur</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="dash-kpi-grid"
|
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "repeat(4, 1fr)",
|
|
||||||
gap: "1rem",
|
|
||||||
marginBottom: "1rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{["Vystaveno", "Zaplaceno", "Po splatnosti", "Celkem"].map(
|
|
||||||
(label, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="dash-kpi-card"
|
|
||||||
style={{
|
|
||||||
padding: "1.25rem",
|
|
||||||
borderRadius: 10,
|
|
||||||
background: "var(--bg-secondary)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ fontSize: "0.875rem", marginBottom: "0.25rem" }}>
|
|
||||||
{label}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: "1.5rem", fontWeight: 600 }}>
|
|
||||||
{i * 5 + 3}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-search-bar mb-4">
|
|
||||||
<input className="admin-form-input" placeholder="" />
|
|
||||||
</div>
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Číslo</th>
|
|
||||||
<th>Zákazník</th>
|
|
||||||
<th>Stav</th>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th className="text-right">Částka</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="admin-mono">FV-2024-00{i + 1}</td>
|
|
||||||
<td>Firma s.r.o.</td>
|
|
||||||
<td>
|
|
||||||
<span className="admin-badge admin-badge-invoice-issued">
|
|
||||||
Vystaveno
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="admin-mono">1. 1. 2024</td>
|
|
||||||
<td className="admin-mono text-right">50 000 Kč</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn-icon">👁</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
export default function LeaveApprovalFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Schvalování dovolené</h1>
|
|
||||||
<p className="admin-page-subtitle">2 čekající</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Uživatel</th>
|
|
||||||
<th>Typ</th>
|
|
||||||
<th>Od</th>
|
|
||||||
<th>Do</th>
|
|
||||||
<th>Dní</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 3 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>Jan Novák</td>
|
|
||||||
<td>Dovolená</td>
|
|
||||||
<td className="admin-mono">1. 7. 2024</td>
|
|
||||||
<td className="admin-mono">5. 7. 2024</td>
|
|
||||||
<td>5</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn admin-btn-sm admin-btn-primary">
|
|
||||||
Schválit
|
|
||||||
</button>
|
|
||||||
<button className="admin-btn admin-btn-sm admin-btn-secondary">
|
|
||||||
Zamítnout
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
export default function LeaveRequestsFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Žádosti o dovolenou</h1>
|
|
||||||
<p className="admin-page-subtitle">3 žádosti</p>
|
|
||||||
</div>
|
|
||||||
<button className="admin-btn admin-btn-primary">+ Nová žádost</button>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Uživatel</th>
|
|
||||||
<th>Typ</th>
|
|
||||||
<th>Od</th>
|
|
||||||
<th>Do</th>
|
|
||||||
<th>Dní</th>
|
|
||||||
<th>Stav</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 3 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>Jan Novák</td>
|
|
||||||
<td>Dovolená</td>
|
|
||||||
<td className="admin-mono">1. 7. 2024</td>
|
|
||||||
<td className="admin-mono">5. 7. 2024</td>
|
|
||||||
<td>5</td>
|
|
||||||
<td>
|
|
||||||
<span className="admin-badge admin-badge-pending">
|
|
||||||
Čeká
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn-icon">Zrušit</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
export default function OfferDetailFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<button className="admin-btn-icon">←</button>
|
|
||||||
<h1 className="admin-page-title">NAB-2024-001</h1>
|
|
||||||
<button className="admin-btn admin-btn-primary">Uložit</button>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div
|
|
||||||
className="admin-form-row"
|
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "1fr 1fr",
|
|
||||||
gap: "1rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Číslo nabídky</label>
|
|
||||||
<div className="admin-form-input">NAB-2024-001</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Zákazník</label>
|
|
||||||
<div className="admin-form-input">Firma s.r.o.</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Datum</label>
|
|
||||||
<div className="admin-form-input">1. 1. 2024</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Platnost do</label>
|
|
||||||
<div className="admin-form-input">31. 1. 2024</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<table className="admin-table" style={{ marginTop: "1rem" }}>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Položka</th>
|
|
||||||
<th>Množství</th>
|
|
||||||
<th>Cena</th>
|
|
||||||
<th>Celkem</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 3 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>Položka {i + 1}</td>
|
|
||||||
<td>1</td>
|
|
||||||
<td>10 000 Kč</td>
|
|
||||||
<td>10 000 Kč</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
export default function OffersCustomersFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Zákazníci</h1>
|
|
||||||
<p className="admin-page-subtitle">8 zákazníků</p>
|
|
||||||
</div>
|
|
||||||
<button className="admin-btn admin-btn-primary">
|
|
||||||
+ Přidat zákazníka
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-search-bar mb-4">
|
|
||||||
<input className="admin-form-input" placeholder="" />
|
|
||||||
</div>
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Název</th>
|
|
||||||
<th>Město</th>
|
|
||||||
<th>IČO</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>Firma s.r.o.</td>
|
|
||||||
<td>Praha</td>
|
|
||||||
<td className="admin-mono">12345678</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn-icon">✎</button>
|
|
||||||
<button className="admin-btn-icon danger">🗑</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
export default function OffersFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Nabídky</h1>
|
|
||||||
<p className="admin-page-subtitle">12 nabídek</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-search-bar mb-4">
|
|
||||||
<input className="admin-form-input" placeholder="" />
|
|
||||||
</div>
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Číslo</th>
|
|
||||||
<th>Zákazník</th>
|
|
||||||
<th>Stav</th>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th className="text-right">Celkem</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="admin-mono">NAB-2024-00{i + 1}</td>
|
|
||||||
<td>Firma s.r.o.</td>
|
|
||||||
<td>
|
|
||||||
<span className="admin-badge admin-badge-offer-active">
|
|
||||||
Aktivní
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="admin-mono">1. 1. 2024</td>
|
|
||||||
<td className="admin-mono text-right fw-500">100 000 Kč</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn-icon">👁</button>
|
|
||||||
<button className="admin-btn-icon">✎</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
export default function OffersTemplatesFixture() {
|
|
||||||
return (
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-search-bar mb-4">
|
|
||||||
<input className="admin-form-input" placeholder="" />
|
|
||||||
</div>
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Název</th>
|
|
||||||
<th>Cena</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>Šablona {i + 1}</td>
|
|
||||||
<td className="admin-mono text-right">1 000 Kč</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn-icon">✎</button>
|
|
||||||
<button className="admin-btn-icon danger">🗑</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
export default function OrderDetailFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div className="admin-page-header-left">
|
|
||||||
<Link
|
|
||||||
to="/orders"
|
|
||||||
className="admin-btn-icon"
|
|
||||||
style={{ marginRight: "0.5rem" }}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<path d="M19 12H5M12 19l-7-7 7-7" />
|
|
||||||
</svg>
|
|
||||||
</Link>
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">OBJ-2024-001</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-page-actions">
|
|
||||||
<button className="admin-btn admin-btn-primary">Uložit</button>
|
|
||||||
<button className="admin-btn admin-btn-secondary">Stornovat</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card" style={{ marginBottom: "1rem" }}>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div
|
|
||||||
className="admin-form-row"
|
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "1fr 1fr",
|
|
||||||
gap: "1rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Zákazník</label>
|
|
||||||
<div>Firma s.r.o.</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Stav</label>
|
|
||||||
<span className="admin-badge admin-badge-order-realizace">
|
|
||||||
V realizaci
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Datum vytvoření</label>
|
|
||||||
<div>1. 1. 2024</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Celkem</label>
|
|
||||||
<div className="fw-500">50 000 Kč</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<h3 className="admin-card-title">Položky</h3>
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Položka</th>
|
|
||||||
<th>Množství</th>
|
|
||||||
<th>Cena</th>
|
|
||||||
<th>Celkem</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 3 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>Položka {i + 1}</td>
|
|
||||||
<td>1</td>
|
|
||||||
<td>10 000 Kč</td>
|
|
||||||
<td>10 000 Kč</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
export default function OrdersFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Objednávky</h1>
|
|
||||||
<p className="admin-page-subtitle">8 objednávek</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-search-bar mb-4">
|
|
||||||
<input className="admin-form-input" placeholder="" />
|
|
||||||
</div>
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Číslo</th>
|
|
||||||
<th>Nabídka</th>
|
|
||||||
<th>Zákazník</th>
|
|
||||||
<th>Stav</th>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th className="text-right">Celkem</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="admin-mono">OBJ-2024-00{i + 1}</td>
|
|
||||||
<td>NAB-2024-00{i + 1}</td>
|
|
||||||
<td>Firma s.r.o.</td>
|
|
||||||
<td>
|
|
||||||
<span className="admin-badge admin-badge-order-realizace">
|
|
||||||
V realizaci
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="admin-mono">1. 1. 2024</td>
|
|
||||||
<td className="admin-mono text-right fw-500">50 000 Kč</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn-icon">👁</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
export default function ProjectDetailFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
|
||||||
<a href="/projects" className="admin-btn-icon">
|
|
||||||
<svg
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<path d="M19 12H5M12 19l-7-7 7-7" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">PRJ-001 Projekt Alpha</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-page-actions">
|
|
||||||
<button className="admin-btn admin-btn-primary">Uložit</button>
|
|
||||||
<button className="admin-btn admin-btn-secondary">Smazat</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card" style={{ marginBottom: "1rem" }}>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div
|
|
||||||
className="admin-form-row"
|
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "1fr 1fr",
|
|
||||||
gap: "1rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Název projektu</label>
|
|
||||||
<input
|
|
||||||
className="admin-form-input"
|
|
||||||
value="Projekt Alpha"
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Stav</label>
|
|
||||||
<select className="admin-form-select">
|
|
||||||
<option>Aktivní</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Začátek</label>
|
|
||||||
<input type="date" className="admin-form-input" readOnly />
|
|
||||||
</div>
|
|
||||||
<div className="admin-form-group">
|
|
||||||
<label className="admin-form-label">Konec</label>
|
|
||||||
<input type="date" className="admin-form-input" readOnly />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card" style={{ marginBottom: "1rem" }}>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<h3 className="admin-card-title">Poznámky</h3>
|
|
||||||
<textarea className="admin-form-input" rows={4} readOnly />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
export default function ProjectFileManagerFixture() {
|
|
||||||
return (
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<h3 className="admin-card-title">Soubory</h3>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "0.5rem",
|
|
||||||
marginBottom: "0.75rem",
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
color: "var(--text-secondary)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>Projekt</span>
|
|
||||||
<span>/</span>
|
|
||||||
<span>Dokumentace</span>
|
|
||||||
</div>
|
|
||||||
{Array.from({ length: 4 }, (_, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "0.5rem",
|
|
||||||
padding: "0.5rem 0",
|
|
||||||
borderBottom: i < 3 ? "1px solid var(--border-color)" : "none",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
>
|
|
||||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
||||||
<polyline points="14 2 14 8 20 8" />
|
|
||||||
</svg>
|
|
||||||
<span style={{ flex: 1 }}>dokument_{i + 1}.pdf</span>
|
|
||||||
<span
|
|
||||||
style={{ color: "var(--text-secondary)", fontSize: "0.8rem" }}
|
|
||||||
>
|
|
||||||
2.{i + 1} MB
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
export default function ProjectsFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Projekty</h1>
|
|
||||||
<p className="admin-page-subtitle">6 projektů</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-search-bar mb-4">
|
|
||||||
<input className="admin-form-input" placeholder="" />
|
|
||||||
</div>
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Číslo</th>
|
|
||||||
<th>Název</th>
|
|
||||||
<th>Zákazník</th>
|
|
||||||
<th>Stav</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="admin-mono">PRJ-2024-00{i + 1}</td>
|
|
||||||
<td>Projekt Alpha</td>
|
|
||||||
<td>Firma s.r.o.</td>
|
|
||||||
<td>
|
|
||||||
<span className="admin-badge admin-badge-project-active">
|
|
||||||
Aktivní
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn-icon">👁</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
export default function ReceivedInvoicesFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Přijaté faktury</h1>
|
|
||||||
<p className="admin-page-subtitle">8 faktur</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="dash-kpi-grid"
|
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "repeat(4, 1fr)",
|
|
||||||
gap: "1rem",
|
|
||||||
marginBottom: "1rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{["K úhradě", "Zaplaceno", "Po splatnosti", "Celkem"].map(
|
|
||||||
(label, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="dash-kpi-card"
|
|
||||||
style={{
|
|
||||||
padding: "1.25rem",
|
|
||||||
borderRadius: 10,
|
|
||||||
background: "var(--bg-secondary)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ fontSize: "0.875rem", marginBottom: "0.25rem" }}>
|
|
||||||
{label}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: "1.5rem", fontWeight: 600 }}>
|
|
||||||
{i * 3 + 2}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Číslo</th>
|
|
||||||
<th>Dodavatel</th>
|
|
||||||
<th>Částka</th>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th>Stav</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="admin-mono">PF-2024-00{i + 1}</td>
|
|
||||||
<td>Dodavatel s.r.o.</td>
|
|
||||||
<td className="admin-mono text-right">10 000 Kč</td>
|
|
||||||
<td className="admin-mono">1. 1. 2024</td>
|
|
||||||
<td>
|
|
||||||
<span className="admin-badge admin-badge-invoice-issued">
|
|
||||||
K úhradě
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn-icon">✎</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
export default function SettingsFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Nastavení</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-tab-bar">
|
|
||||||
<button className="admin-tab active">Role</button>
|
|
||||||
<button className="admin-tab">Systém</button>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Název role</th>
|
|
||||||
<th>Popis</th>
|
|
||||||
<th>Oprávnění</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 3 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="fw-500">admin</td>
|
|
||||||
<td>Správa celého systému</td>
|
|
||||||
<td>
|
|
||||||
<span className="admin-badge admin-badge-info">
|
|
||||||
všechna
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn-icon" title="Upravit">
|
|
||||||
✎
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-header">
|
|
||||||
<h2 className="admin-card-title">Docházka</h2>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-form">
|
|
||||||
<div className="admin-form-row">
|
|
||||||
<label className="admin-form-label">
|
|
||||||
Limit pro přestávku (hodiny)
|
|
||||||
</label>
|
|
||||||
<input className="admin-form-input" readOnly value="6" />
|
|
||||||
<label className="admin-form-label">
|
|
||||||
Délka krátké přestávky (min)
|
|
||||||
</label>
|
|
||||||
<input className="admin-form-input" readOnly value="15" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
export default function TripsAdminFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Správa jízd</h1>
|
|
||||||
<p className="admin-page-subtitle">10 jízd</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th>Vozidlo</th>
|
|
||||||
<th>Uživatel</th>
|
|
||||||
<th>Km</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="admin-mono">1. 1. 2024</td>
|
|
||||||
<td>Škoda Octavia</td>
|
|
||||||
<td>Jan Novák</td>
|
|
||||||
<td className="admin-mono">200</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn-icon">✎</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
export default function TripsFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Kniha jízd</h1>
|
|
||||||
<p className="admin-page-subtitle">duben 2025</p>
|
|
||||||
</div>
|
|
||||||
<div className="admin-page-actions">
|
|
||||||
<button className="admin-btn admin-btn-primary">Přidat jízdu</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-grid admin-grid-4">
|
|
||||||
<div className="admin-stat-card info">
|
|
||||||
<div className="admin-stat-icon info">
|
|
||||||
<svg
|
|
||||||
width="22"
|
|
||||||
height="22"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<line x1="12" y1="20" x2="12" y2="10" />
|
|
||||||
<line x1="18" y1="20" x2="18" y2="4" />
|
|
||||||
<line x1="6" y1="20" x2="6" y2="16" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="admin-stat-content">
|
|
||||||
<span className="admin-stat-value">12</span>
|
|
||||||
<span className="admin-stat-label">Počet jízd</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-stat-card">
|
|
||||||
<div className="admin-stat-icon">
|
|
||||||
<svg
|
|
||||||
width="22"
|
|
||||||
height="22"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="admin-stat-content">
|
|
||||||
<span className="admin-stat-value">1 240 km</span>
|
|
||||||
<span className="admin-stat-label">Celkem naježděno</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-stat-card success">
|
|
||||||
<div className="admin-stat-icon success">
|
|
||||||
<svg
|
|
||||||
width="22"
|
|
||||||
height="22"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<rect x="1" y="3" width="15" height="13" rx="2" ry="2" />
|
|
||||||
<path d="M16 8h2a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-1" />
|
|
||||||
<circle cx="5.5" cy="18" r="2" />
|
|
||||||
<circle cx="18.5" cy="18" r="2" />
|
|
||||||
<path d="M8 18h8" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="admin-stat-content">
|
|
||||||
<span className="admin-stat-value">980 km</span>
|
|
||||||
<span className="admin-stat-label">Služební</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-stat-card warning">
|
|
||||||
<div className="admin-stat-icon warning">
|
|
||||||
<svg
|
|
||||||
width="22"
|
|
||||||
height="22"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
|
||||||
<polyline points="9 22 9 12 15 12 15 22" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="admin-stat-content">
|
|
||||||
<span className="admin-stat-value">260 km</span>
|
|
||||||
<span className="admin-stat-label">Soukromé</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card mt-6">
|
|
||||||
<div className="admin-card-header flex-between">
|
|
||||||
<h2 className="admin-card-title">Poslední jízdy</h2>
|
|
||||||
<a className="admin-btn admin-btn-secondary admin-btn-sm">
|
|
||||||
Zobrazit historii
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th>Vozidlo</th>
|
|
||||||
<th>Trasa</th>
|
|
||||||
<th>Vzdálenost</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 4 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="admin-mono">1. 4. 2025</td>
|
|
||||||
<td>
|
|
||||||
<span className="admin-badge">1A2 3456</span>
|
|
||||||
</td>
|
|
||||||
<td>Praha → Brno</td>
|
|
||||||
<td className="admin-mono">
|
|
||||||
<strong>200 km</strong>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
export default function TripsHistoryFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Historie jízd</h1>
|
|
||||||
<p className="admin-page-subtitle">10 jízd</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Datum</th>
|
|
||||||
<th>Vozidlo</th>
|
|
||||||
<th>Uživatel</th>
|
|
||||||
<th>Trasa</th>
|
|
||||||
<th>Km</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td className="admin-mono">1. 1. 2024</td>
|
|
||||||
<td>Škoda Octavia</td>
|
|
||||||
<td>Jan Novák</td>
|
|
||||||
<td>Praha → Brno</td>
|
|
||||||
<td className="admin-mono">200</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
export default function UsersFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<div>
|
|
||||||
<h1 className="admin-page-title">Uživatelé</h1>
|
|
||||||
<p className="admin-page-subtitle">5 uživatelů</p>
|
|
||||||
</div>
|
|
||||||
<button className="admin-btn admin-btn-primary">
|
|
||||||
+ Přidat uživatele
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-search-bar mb-4">
|
|
||||||
<input className="admin-form-input" placeholder="" />
|
|
||||||
</div>
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Uživatel</th>
|
|
||||||
<th>E-mail</th>
|
|
||||||
<th>Role</th>
|
|
||||||
<th>Stav</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-user">
|
|
||||||
<div className="admin-table-avatar">A</div>
|
|
||||||
<div>
|
|
||||||
<div className="admin-table-name">Jan Novák</div>
|
|
||||||
<div className="admin-table-username">@jan</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>jan@email.cz</td>
|
|
||||||
<td>
|
|
||||||
<span className="admin-badge admin-badge-admin">
|
|
||||||
Administrátor
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span className="admin-badge admin-badge-active">
|
|
||||||
Aktivní
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn-icon">✎</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
export default function VehiclesFixture() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="admin-page-header">
|
|
||||||
<h1 className="admin-page-title">Vozidla</h1>
|
|
||||||
<button className="admin-btn admin-btn-primary">
|
|
||||||
+ Přidat vozidlo
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="admin-card">
|
|
||||||
<div className="admin-card-body">
|
|
||||||
<div className="admin-search-bar mb-4">
|
|
||||||
<input className="admin-form-input" placeholder="" />
|
|
||||||
</div>
|
|
||||||
<div className="admin-table-responsive">
|
|
||||||
<table className="admin-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Značka</th>
|
|
||||||
<th>Model</th>
|
|
||||||
<th>SPZ</th>
|
|
||||||
<th>Rok</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>Škoda</td>
|
|
||||||
<td>Octavia</td>
|
|
||||||
<td className="admin-mono">1A2 3456</td>
|
|
||||||
<td>2024</td>
|
|
||||||
<td>
|
|
||||||
<div className="admin-table-actions">
|
|
||||||
<button className="admin-btn-icon">✎</button>
|
|
||||||
<button className="admin-btn-icon danger">🗑</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
|
import { isHoliday } from "../../utils/czech-holidays";
|
||||||
import {
|
import {
|
||||||
calcProjectMinutesTotal,
|
calcProjectMinutesTotal,
|
||||||
calcFormWorkMinutes,
|
calcFormWorkMinutes,
|
||||||
@@ -10,9 +11,17 @@ import {
|
|||||||
formatDate,
|
formatDate,
|
||||||
formatMinutes,
|
formatMinutes,
|
||||||
getLeaveTypeName,
|
getLeaveTypeName,
|
||||||
getLeaveTypeBadgeClass,
|
|
||||||
formatTimeOrDatetimePrint,
|
formatTimeOrDatetimePrint,
|
||||||
calculateWorkMinutesPrint,
|
calculateWorkMinutesPrint,
|
||||||
|
isWeekendDate,
|
||||||
|
getDaysInMonth,
|
||||||
|
shiftDateForMonth,
|
||||||
|
formatHoursDecimal,
|
||||||
|
calculateNightMinutes,
|
||||||
|
normalizeDateStr,
|
||||||
|
holidaysInMonth,
|
||||||
|
calculateFreeHolidayHours,
|
||||||
|
getCzechWeekday,
|
||||||
} from "../utils/attendanceHelpers";
|
} from "../utils/attendanceHelpers";
|
||||||
import type {
|
import type {
|
||||||
ShiftFormData,
|
ShiftFormData,
|
||||||
@@ -29,7 +38,7 @@ interface AlertContext {
|
|||||||
alert: { success: (msg: string) => void; error: (msg: string) => void };
|
alert: { success: (msg: string) => void; error: (msg: string) => void };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AttendanceRecord {
|
export interface AttendanceRecord {
|
||||||
id: number;
|
id: number;
|
||||||
user_id: number;
|
user_id: number;
|
||||||
shift_date: string;
|
shift_date: string;
|
||||||
@@ -39,7 +48,7 @@ interface AttendanceRecord {
|
|||||||
departure_time?: string | null;
|
departure_time?: string | null;
|
||||||
break_start?: string | null;
|
break_start?: string | null;
|
||||||
break_end?: string | null;
|
break_end?: string | null;
|
||||||
notes?: string;
|
notes?: string | null;
|
||||||
project_id?: number | null;
|
project_id?: number | null;
|
||||||
project_name?: string;
|
project_name?: string;
|
||||||
project_logs?: Array<{
|
project_logs?: Array<{
|
||||||
@@ -73,7 +82,6 @@ interface UserTotal {
|
|||||||
working: boolean;
|
working: boolean;
|
||||||
vacation_hours: number;
|
vacation_hours: number;
|
||||||
sick_hours: number;
|
sick_hours: number;
|
||||||
holiday_hours: number;
|
|
||||||
unpaid_hours: number;
|
unpaid_hours: number;
|
||||||
overtime: number;
|
overtime: number;
|
||||||
missing: number;
|
missing: number;
|
||||||
@@ -81,6 +89,16 @@ interface UserTotal {
|
|||||||
business_days: number;
|
business_days: number;
|
||||||
worked_hours: number;
|
worked_hours: number;
|
||||||
covered: number;
|
covered: number;
|
||||||
|
// mzda-style summary fields (set by computeUserTotals)
|
||||||
|
worked_hours_raw?: number;
|
||||||
|
odpracovano?: number;
|
||||||
|
vcetne_svatku?: number;
|
||||||
|
prescas?: number;
|
||||||
|
svatek?: number;
|
||||||
|
weekend_hours?: number;
|
||||||
|
night_minutes?: number;
|
||||||
|
worked_holiday_hours?: number; // sum of hours worked ON a holiday
|
||||||
|
records?: AttendanceRecord[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LeaveBalance {
|
interface LeaveBalance {
|
||||||
@@ -121,6 +139,11 @@ const combineDatetime = (date: string, time: string): string | null =>
|
|||||||
/**
|
/**
|
||||||
* Compute per-user totals from raw attendance records.
|
* Compute per-user totals from raw attendance records.
|
||||||
* This replaces the server-side `user_totals` that the PHP backend returned.
|
* This replaces the server-side `user_totals` that the PHP backend returned.
|
||||||
|
*
|
||||||
|
* Adds mzda-style summary fields (odpracovano, vcetne_svatku, prescas, svatek,
|
||||||
|
* weekend_hours, night_minutes) and a fund computed as
|
||||||
|
* (rawBizDays + holidayCount) × 8 — holidays count as work days for fund
|
||||||
|
* purposes, per the mzda.pdf model.
|
||||||
*/
|
*/
|
||||||
function computeUserTotals(
|
function computeUserTotals(
|
||||||
records: AttendanceRecord[],
|
records: AttendanceRecord[],
|
||||||
@@ -128,6 +151,7 @@ function computeUserTotals(
|
|||||||
month: string,
|
month: string,
|
||||||
): Record<string, UserTotal> {
|
): Record<string, UserTotal> {
|
||||||
const totals: Record<string, UserTotal> = {};
|
const totals: Record<string, UserTotal> = {};
|
||||||
|
const recordsByUser = new Map<number, AttendanceRecord[]>();
|
||||||
|
|
||||||
for (const rec of records) {
|
for (const rec of records) {
|
||||||
const uid = String(rec.user_id);
|
const uid = String(rec.user_id);
|
||||||
@@ -144,7 +168,6 @@ function computeUserTotals(
|
|||||||
working: false,
|
working: false,
|
||||||
vacation_hours: 0,
|
vacation_hours: 0,
|
||||||
sick_hours: 0,
|
sick_hours: 0,
|
||||||
holiday_hours: 0,
|
|
||||||
unpaid_hours: 0,
|
unpaid_hours: 0,
|
||||||
overtime: 0,
|
overtime: 0,
|
||||||
missing: 0,
|
missing: 0,
|
||||||
@@ -152,15 +175,23 @@ function computeUserTotals(
|
|||||||
business_days: 0,
|
business_days: 0,
|
||||||
worked_hours: 0,
|
worked_hours: 0,
|
||||||
covered: 0,
|
covered: 0,
|
||||||
|
records: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = totals[uid];
|
const t = totals[uid];
|
||||||
|
t.records!.push(rec);
|
||||||
|
if (!recordsByUser.has(rec.user_id)) recordsByUser.set(rec.user_id, []);
|
||||||
|
recordsByUser.get(rec.user_id)!.push(rec);
|
||||||
|
|
||||||
const leaveType = rec.leave_type || "work";
|
const leaveType = rec.leave_type || "work";
|
||||||
|
|
||||||
if (leaveType === "work") {
|
if (leaveType === "work") {
|
||||||
// Only work records contribute to "minutes" (matching PHP calculateUserTotals)
|
// Only work records contribute to "minutes" (matching PHP calculateUserTotals)
|
||||||
t.minutes += calculateWorkMinutes(rec);
|
t.minutes += calculateWorkMinutes({
|
||||||
|
...rec,
|
||||||
|
notes: rec.notes ?? undefined,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const leaveHours = Number(rec.leave_hours) || 8;
|
const leaveHours = Number(rec.leave_hours) || 8;
|
||||||
switch (leaveType) {
|
switch (leaveType) {
|
||||||
@@ -170,12 +201,14 @@ function computeUserTotals(
|
|||||||
case "sick":
|
case "sick":
|
||||||
t.sick_hours += leaveHours;
|
t.sick_hours += leaveHours;
|
||||||
break;
|
break;
|
||||||
case "holiday":
|
|
||||||
t.holiday_hours += leaveHours;
|
|
||||||
break;
|
|
||||||
case "unpaid":
|
case "unpaid":
|
||||||
t.unpaid_hours += leaveHours;
|
t.unpaid_hours += leaveHours;
|
||||||
break;
|
break;
|
||||||
|
// "holiday" leave_type is no longer selectable in the UI — it is
|
||||||
|
// auto-computed from Czech public holidays (see freeHolidayHours
|
||||||
|
// below). Old records may still exist in the DB; treat them as a
|
||||||
|
// paid non-work entry and skip so they don't double-count with
|
||||||
|
// the auto-detected svátek.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +223,10 @@ function computeUserTotals(
|
|||||||
const yr = parseInt(yearStr, 10);
|
const yr = parseInt(yearStr, 10);
|
||||||
const mo = parseInt(monthStr, 10) - 1;
|
const mo = parseInt(monthStr, 10) - 1;
|
||||||
|
|
||||||
// Count business days in month (Mon-Fri)
|
// Count Mon-Fri business days in month (INCLUDING holidays).
|
||||||
|
// The fund is rawBizDays × 8 — holidays stay in the count so the
|
||||||
|
// user is paid for them via the fund. Svátek (free holiday hours) is
|
||||||
|
// computed separately as "8h per holiday the user did not work".
|
||||||
let rawBizDays = 0;
|
let rawBizDays = 0;
|
||||||
const cur = new Date(yr, mo, 1);
|
const cur = new Date(yr, mo, 1);
|
||||||
while (cur.getMonth() === mo) {
|
while (cur.getMonth() === mo) {
|
||||||
@@ -199,23 +235,100 @@ function computeUserTotals(
|
|||||||
cur.setDate(cur.getDate() + 1);
|
cur.setDate(cur.getDate() + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const holidayDates = holidaysInMonth(yr, mo + 1);
|
||||||
|
const holidayCount = holidayDates.length;
|
||||||
|
|
||||||
for (const uid of Object.keys(totals)) {
|
for (const uid of Object.keys(totals)) {
|
||||||
const t = totals[uid];
|
const t = totals[uid];
|
||||||
// Subtract holiday days from business days for this user
|
const userRecords = recordsByUser.get(Number(uid)) || [];
|
||||||
const holidayDays = Math.round(t.holiday_hours / 8);
|
|
||||||
const bizDays = Math.max(0, rawBizDays - holidayDays);
|
// Odpracováno: floor(worked / 8) × 8 — round to whole days, drop remainder.
|
||||||
const fund = bizDays * 8;
|
const workedHoursRaw = t.minutes / 60;
|
||||||
const workedHours = Math.round((t.minutes / 60) * 10) / 10;
|
const odpracovano = Math.floor(workedHoursRaw / 8) * 8;
|
||||||
// Covered = worked + vacation + sick (NOT holiday/unpaid — matching PHP)
|
|
||||||
const leaveHours = t.vacation_hours + t.sick_hours;
|
// So/Ne: sum of worked hours on Sat/Sun (raw, not rounded).
|
||||||
const covered = Math.round((workedHours + leaveHours) * 10) / 10;
|
const weekendHours = userRecords
|
||||||
|
.filter(
|
||||||
|
(r) =>
|
||||||
|
(r.leave_type || "work") === "work" && isWeekendDate(r.shift_date),
|
||||||
|
)
|
||||||
|
.reduce((sum, r) => sum + calculateWorkMinutesPrint(r) / 60, 0);
|
||||||
|
|
||||||
|
// Práce v noci: minutes in 22:00-06:00 across all work records.
|
||||||
|
const nightMinutes = userRecords
|
||||||
|
.filter((r) => (r.leave_type || "work") === "work")
|
||||||
|
.reduce((sum, r) => sum + calculateNightMinutes(r), 0);
|
||||||
|
|
||||||
|
// Svátek (free): 8h per holiday the user did not work.
|
||||||
|
const freeHolidayHours = calculateFreeHolidayHours(
|
||||||
|
userRecords,
|
||||||
|
holidayDates,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Worked holidays: holidays the user DID work (counted toward
|
||||||
|
// Odpracováno, but their 8h must not flow into Přesčas).
|
||||||
|
const workedDatesSet = new Set(
|
||||||
|
userRecords
|
||||||
|
.filter((r) => (r.leave_type || "work") === "work")
|
||||||
|
.map((r) => normalizeDateStr(r.shift_date))
|
||||||
|
.filter(Boolean),
|
||||||
|
);
|
||||||
|
let workedHolidayCount = 0;
|
||||||
|
let workedHolidayHours = 0;
|
||||||
|
for (const hd of holidayDates) {
|
||||||
|
if (workedDatesSet.has(hd)) workedHolidayCount++;
|
||||||
|
}
|
||||||
|
// Sum the actual hours worked on holiday dates (for the KPI card's
|
||||||
|
// "fond used" total = real_work + vacation + worked_holiday + free_holiday).
|
||||||
|
for (const r of userRecords) {
|
||||||
|
if ((r.leave_type || "work") !== "work") continue;
|
||||||
|
const d = normalizeDateStr(r.shift_date);
|
||||||
|
if (d && holidayDates.includes(d)) {
|
||||||
|
workedHolidayHours += calculateWorkMinutesPrint(r) / 60;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fund = Mon-Fri × 8h. Holidays count as work days (already in rawBizDays).
|
||||||
|
const fund = rawBizDays * 8;
|
||||||
|
|
||||||
|
// Včetně svátků a přesčasů (mzda formula):
|
||||||
|
// odpracovano + vacation + remainder − min(freeHols, workedHols × 8)
|
||||||
|
//
|
||||||
|
// Two-way logic matching the print:
|
||||||
|
// • If the user worked at least one holiday, that worked holiday's
|
||||||
|
// 8h is in Odpracováno and shouldn't also count as Přesčas. We
|
||||||
|
// subtract one 8h per worked holiday (capped at the total free
|
||||||
|
// holiday hours — can't go negative).
|
||||||
|
// • If the user did NOT work any holiday, no adjustment is needed:
|
||||||
|
// all unworked holidays are already paid via the Svátek line and
|
||||||
|
// don't flow into Přesčas, so vcetne_svatku just equals the
|
||||||
|
// work + vacation + remainder.
|
||||||
|
const remainder = workedHoursRaw - odpracovano;
|
||||||
|
const holidayAdj = Math.min(freeHolidayHours, workedHolidayCount * 8);
|
||||||
|
const vcetneSv = odpracovano - holidayAdj + t.vacation_hours + remainder;
|
||||||
|
|
||||||
|
// Přesčas = vcetneSv − odpracovano (derived).
|
||||||
|
const prescas = vcetneSv - odpracovano;
|
||||||
|
|
||||||
|
// Legacy fields (kept so existing UI doesn't break).
|
||||||
|
const workedHours = Math.round(workedHoursRaw * 10) / 10;
|
||||||
|
const covered = Math.round((workedHours + t.vacation_hours) * 10) / 10;
|
||||||
|
|
||||||
t.fund = fund;
|
t.fund = fund;
|
||||||
t.business_days = bizDays;
|
t.business_days = rawBizDays;
|
||||||
t.worked_hours = workedHours;
|
t.worked_hours = workedHours;
|
||||||
t.covered = covered;
|
t.covered = covered;
|
||||||
t.missing = Math.max(0, Math.round((fund - covered) * 10) / 10);
|
t.missing = Math.max(0, Math.round((fund - covered) * 10) / 10);
|
||||||
t.overtime = Math.max(0, Math.round((covered - fund) * 10) / 10);
|
t.overtime = Math.max(0, Math.round((covered - fund) * 10) / 10);
|
||||||
|
|
||||||
|
t.worked_hours_raw = workedHoursRaw;
|
||||||
|
t.odpracovano = odpracovano;
|
||||||
|
t.vcetne_svatku = vcetneSv;
|
||||||
|
t.prescas = prescas;
|
||||||
|
t.svatek = freeHolidayHours;
|
||||||
|
t.weekend_hours = weekendHours;
|
||||||
|
t.night_minutes = nightMinutes;
|
||||||
|
t.worked_holiday_hours = workedHolidayHours;
|
||||||
}
|
}
|
||||||
|
|
||||||
return totals;
|
return totals;
|
||||||
@@ -234,14 +347,6 @@ function escapeHtml(str: string): string {
|
|||||||
.replace(/'/g, "'");
|
.replace(/'/g, "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFundStatus(userData: Record<string, any>): string {
|
|
||||||
if (userData.overtime > 0)
|
|
||||||
return `<span class="leave-badge badge-overtime">+${escapeHtml(String(userData.overtime))}h přesčas</span>`;
|
|
||||||
if (userData.missing > 0)
|
|
||||||
return `<span style="color:#dc2626">−${escapeHtml(String(userData.missing))}h</span>`;
|
|
||||||
return '<span style="color:#16a34a">splněno</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildProjectLogsHtml(record: Record<string, any>): string {
|
function buildProjectLogsHtml(record: Record<string, any>): string {
|
||||||
if (record.project_logs && record.project_logs.length > 0) {
|
if (record.project_logs && record.project_logs.length > 0) {
|
||||||
return record.project_logs
|
return record.project_logs
|
||||||
@@ -272,36 +377,55 @@ function buildProjectLogsHtml(record: Record<string, any>): string {
|
|||||||
return escapeHtml(record.project_name || "—");
|
return escapeHtml(record.project_name || "—");
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildLeaveSummaryHtml(
|
|
||||||
userId: string,
|
|
||||||
userData: Record<string, any>,
|
|
||||||
printData: Record<string, any>,
|
|
||||||
): string {
|
|
||||||
const bal = printData.leave_balances[userId];
|
|
||||||
let parts = `<strong>Dovolená ${escapeHtml(String(printData.year))}:</strong> Zbývá ${bal.vacation_remaining.toFixed(1)}h z ${bal.vacation_total}h`;
|
|
||||||
if (userData.vacation_hours > 0)
|
|
||||||
parts += ` | <span class="leave-badge badge-vacation">Tento měsíc: ${escapeHtml(String(userData.vacation_hours))}h</span>`;
|
|
||||||
if (userData.sick_hours > 0)
|
|
||||||
parts += ` | <span class="leave-badge badge-sick">Nemoc: ${escapeHtml(String(userData.sick_hours))}h</span>`;
|
|
||||||
if (userData.holiday_hours > 0)
|
|
||||||
parts += ` | <span class="leave-badge badge-holiday">Svátek: ${escapeHtml(String(userData.holiday_hours))}h</span>`;
|
|
||||||
if (userData.overtime > 0)
|
|
||||||
parts += ` | <span class="leave-badge badge-overtime">Přesčas: +${escapeHtml(String(userData.overtime))}h</span>`;
|
|
||||||
return `<div class="leave-summary">${parts}</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildUserSectionHtml(
|
function buildUserSectionHtml(
|
||||||
userId: string,
|
userId: string,
|
||||||
userData: Record<string, any>,
|
userData: Record<string, any>,
|
||||||
printData: Record<string, any>,
|
printData: Record<string, any>,
|
||||||
): string {
|
): string {
|
||||||
const leaveHtml = printData.leave_balances[userId]
|
// Build a date-keyed lookup of the user's records.
|
||||||
? buildLeaveSummaryHtml(userId, userData, printData)
|
const recordsByDate = new Map<string, Record<string, any>>();
|
||||||
: "";
|
for (const r of userData.records || []) {
|
||||||
|
recordsByDate.set(normalizeDateStr(r.shift_date), r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate one row per day of the month.
|
||||||
|
const [yearStr, monthStr] = printData.month.split("-");
|
||||||
|
const yr = parseInt(yearStr, 10);
|
||||||
|
const mo = parseInt(monthStr, 10);
|
||||||
|
const daysInMonth = getDaysInMonth(yr, mo);
|
||||||
|
|
||||||
|
const userRecords = (userData.records || []) as Record<string, any>[];
|
||||||
|
|
||||||
|
const rows: string[] = [];
|
||||||
|
for (let d = 1; d <= daysInMonth; d++) {
|
||||||
|
const dateStr = shiftDateForMonth(yr, mo, d);
|
||||||
|
const record = recordsByDate.get(dateStr);
|
||||||
|
const weekend = isWeekendDate(dateStr);
|
||||||
|
const holiday = isHoliday(dateStr);
|
||||||
|
const leaveType = (record?.leave_type as string) || "work";
|
||||||
|
const rowClasses = [
|
||||||
|
weekend && "weekend",
|
||||||
|
holiday && "holiday",
|
||||||
|
leaveType === "vacation" && "vacation",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
rows.push(`<tr class="${rowClasses}">
|
||||||
|
<td>${escapeHtml(formatDate(dateStr))}</td>
|
||||||
|
<td>${escapeHtml(getCzechWeekday(dateStr))}</td>
|
||||||
|
<td>—</td>
|
||||||
|
<td class="text-center">—</td>
|
||||||
|
<td class="text-center">—</td>
|
||||||
|
<td class="text-center">—</td>
|
||||||
|
<td class="text-center">—</td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const recordRows = (userData.records || [])
|
|
||||||
.map((record: any) => {
|
|
||||||
const leaveType = record.leave_type || "work";
|
|
||||||
const isLeave = leaveType !== "work";
|
const isLeave = leaveType !== "work";
|
||||||
const workMinutes = calculateWorkMinutesPrint(record);
|
const workMinutes = calculateWorkMinutesPrint(record);
|
||||||
const hours = Math.floor(workMinutes / 60);
|
const hours = Math.floor(workMinutes / 60);
|
||||||
@@ -311,55 +435,162 @@ function buildUserSectionHtml(
|
|||||||
? "—"
|
? "—"
|
||||||
: `${escapeHtml(formatTimeOrDatetimePrint(record.break_start, record.shift_date))} - ${escapeHtml(formatTimeOrDatetimePrint(record.break_end, record.shift_date))}`;
|
: `${escapeHtml(formatTimeOrDatetimePrint(record.break_start, record.shift_date))} - ${escapeHtml(formatTimeOrDatetimePrint(record.break_end, record.shift_date))}`;
|
||||||
|
|
||||||
return `<tr>
|
let hoursCell: string;
|
||||||
<td>${escapeHtml(formatDate(record.shift_date))}</td>
|
if (workMinutes > 0 && !isLeave) {
|
||||||
<td><span class="leave-badge ${escapeHtml(getLeaveTypeBadgeClass(leaveType))}">${escapeHtml(getLeaveTypeName(leaveType))}</span></td>
|
hoursCell = `${hours}:${String(mins).padStart(2, "0")} (${formatHoursDecimal(workMinutes)})`;
|
||||||
|
} else if (isLeave) {
|
||||||
|
hoursCell = formatHoursDecimal((Number(record.leave_hours) || 8) * 60);
|
||||||
|
} else {
|
||||||
|
hoursCell = "—";
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.push(`<tr class="${rowClasses}">
|
||||||
|
<td>${escapeHtml(formatDate(dateStr))}</td>
|
||||||
|
<td>${escapeHtml(getCzechWeekday(dateStr))}</td>
|
||||||
|
<td>${escapeHtml(getLeaveTypeName(leaveType))}</td>
|
||||||
<td class="text-center">${isLeave ? "—" : escapeHtml(formatTimeOrDatetimePrint(record.arrival_time, record.shift_date))}</td>
|
<td class="text-center">${isLeave ? "—" : escapeHtml(formatTimeOrDatetimePrint(record.arrival_time, record.shift_date))}</td>
|
||||||
<td class="text-center">${breakCell}</td>
|
<td class="text-center">${breakCell}</td>
|
||||||
<td class="text-center">${isLeave ? "—" : escapeHtml(formatTimeOrDatetimePrint(record.departure_time, record.shift_date))}</td>
|
<td class="text-center">${isLeave ? "—" : escapeHtml(formatTimeOrDatetimePrint(record.departure_time, record.shift_date))}</td>
|
||||||
<td class="text-center">${workMinutes > 0 ? `${hours}:${String(mins).padStart(2, "0")}` : "—"}</td>
|
<td class="text-center">${hoursCell}</td>
|
||||||
<td style="font-size:8px">${buildProjectLogsHtml(record)}</td>
|
<td style="font-size:8px">${buildProjectLogsHtml(record)}</td>
|
||||||
<td>${escapeHtml(record.notes || "")}</td>
|
<td>${escapeHtml(record.notes || "")}</td>
|
||||||
</tr>`;
|
</tr>`);
|
||||||
})
|
}
|
||||||
.join("");
|
|
||||||
|
|
||||||
const fundRow =
|
// ----- mzda-style summary numbers (computed here, not from userData,
|
||||||
userData.fund !== null
|
// because the backend's getPrintData only returns the legacy fields). -----
|
||||||
? `<tr>
|
|
||||||
<td colspan="6" class="text-right">Fond měsíce:</td>
|
// Worked minutes: sum of calculateWorkMinutesPrint across work records.
|
||||||
<td class="text-center">${escapeHtml(String(userData.covered))}h / ${escapeHtml(String(userData.fund))}h</td>
|
let workedMinutes = 0;
|
||||||
<td colspan="2">${renderFundStatus(userData)}</td>
|
let weekendHoursRaw = 0;
|
||||||
</tr>`
|
let nightMinutes = 0;
|
||||||
: "";
|
for (const r of userRecords) {
|
||||||
|
const leaveType = r.leave_type || "work";
|
||||||
|
if (leaveType !== "work") continue;
|
||||||
|
const m = calculateWorkMinutesPrint(r);
|
||||||
|
workedMinutes += m;
|
||||||
|
if (isWeekendDate(r.shift_date)) {
|
||||||
|
weekendHoursRaw += m / 60;
|
||||||
|
}
|
||||||
|
nightMinutes += calculateNightMinutes(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum of vacation/sick/unpaid hours (in hours, not minutes). The
|
||||||
|
// "holiday" leave_type is auto-computed from Czech holidays further down
|
||||||
|
// and no longer selectable in the UI, so it's deliberately omitted here.
|
||||||
|
let vacationHours = 0,
|
||||||
|
sickHours = 0;
|
||||||
|
for (const r of userRecords) {
|
||||||
|
const lt = r.leave_type || "work";
|
||||||
|
if (lt === "work") continue;
|
||||||
|
const h = Number(r.leave_hours) || 8;
|
||||||
|
if (lt === "vacation") vacationHours += h;
|
||||||
|
else if (lt === "sick") sickHours += h;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Odpracováno: floor(worked / 8) × 8.
|
||||||
|
const workedHoursRaw = workedMinutes / 60;
|
||||||
|
const odpracovano = Math.floor(workedHoursRaw / 8) * 8;
|
||||||
|
|
||||||
|
// Free Svátek: 8h per holiday date the user did not work.
|
||||||
|
const holidayDates = holidaysInMonth(yr, mo);
|
||||||
|
const holidayCount = holidayDates.length;
|
||||||
|
const workedDates = new Set(
|
||||||
|
userRecords
|
||||||
|
.filter((r) => (r.leave_type || "work") === "work")
|
||||||
|
.map((r) => normalizeDateStr(r.shift_date))
|
||||||
|
.filter(Boolean),
|
||||||
|
);
|
||||||
|
let freeHolidayHours = 0;
|
||||||
|
let workedHolidayCount = 0;
|
||||||
|
for (const hd of holidayDates) {
|
||||||
|
if (workedDates.has(hd)) workedHolidayCount++;
|
||||||
|
else freeHolidayHours += 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fund: count Mon-Fri (incl. holidays) × 8h.
|
||||||
|
let rawBizDays = 0;
|
||||||
|
const cur = new Date(yr, mo - 1, 1);
|
||||||
|
while (cur.getMonth() === mo - 1) {
|
||||||
|
const dow = cur.getDay();
|
||||||
|
if (dow !== 0 && dow !== 6) rawBizDays++;
|
||||||
|
cur.setDate(cur.getDate() + 1);
|
||||||
|
}
|
||||||
|
const businessDays = rawBizDays;
|
||||||
|
const fund = businessDays * 8;
|
||||||
|
|
||||||
|
// Včetně svátků a přesčasů: floor(worked/8)*8 − worked_holiday*8 + vacation + remainder.
|
||||||
|
// (A worked holiday counts toward Odpracováno, but its 8h should not flow
|
||||||
|
// into Přesčas — so we subtract it here. Unworked holidays are paid via
|
||||||
|
// the fund and don't affect this line.)
|
||||||
|
const remainder = workedHoursRaw - odpracovano;
|
||||||
|
const vcetneSv =
|
||||||
|
odpracovano - workedHolidayCount * 8 + vacationHours + remainder;
|
||||||
|
|
||||||
|
// Přesčas = #2 - #1.
|
||||||
|
const prescas = vcetneSv - odpracovano;
|
||||||
|
|
||||||
|
const summaryHtml = `<div class="user-summary">
|
||||||
|
<div class="summary-row">
|
||||||
|
<span class="summary-label">Odpracováno:</span>
|
||||||
|
<span class="summary-value">${formatHoursDecimal(odpracovano * 60)}h</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span class="summary-label">Odpracováno včetně svátků a přesčasů:</span>
|
||||||
|
<span class="summary-value">${formatHoursDecimal(vcetneSv * 60)}h</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span class="summary-label">Dovolená:</span>
|
||||||
|
<span class="summary-value">${formatHoursDecimal(vacationHours * 60)}h</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span class="summary-label">Přesčas:</span>
|
||||||
|
<span class="summary-value">${formatHoursDecimal(prescas * 60)}h</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span class="summary-label">Svátek:</span>
|
||||||
|
<span class="summary-value">${formatHoursDecimal(freeHolidayHours * 60)}h</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span class="summary-label">So/Ne:</span>
|
||||||
|
<span class="summary-value">${formatHoursDecimal(weekendHoursRaw * 60)}h</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span class="summary-label">Práce v noci:</span>
|
||||||
|
<span class="summary-value">${formatHoursDecimal(nightMinutes)}h</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row summary-fund">
|
||||||
|
<span class="summary-label">Fond měsíce:</span>
|
||||||
|
<span class="summary-value">${formatHoursDecimal(fund * 60)}h (${businessDays} dnů)</span>
|
||||||
|
</div>
|
||||||
|
<div class="legend">
|
||||||
|
<span class="legend-item"><span class="legend-swatch weekend"></span>Víkend (So/Ne)</span>
|
||||||
|
<span class="legend-item"><span class="legend-swatch holiday"></span>Svátek</span>
|
||||||
|
<span class="legend-item"><span class="legend-swatch weekend-holiday"></span>Víkend + svátek</span>
|
||||||
|
<span class="legend-item"><span class="legend-swatch vacation"></span>Dovolená</span>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
return `<div class="user-section">
|
return `<div class="user-section">
|
||||||
<div class="user-header">
|
<div class="user-header">
|
||||||
<h3>${escapeHtml(userData.name)}</h3>
|
<h3>${escapeHtml(userData.name)}</h3>
|
||||||
<span class="total">Odpracováno: ${escapeHtml(formatMinutes(userData.minutes))} h</span>
|
<span class="total">Odpracováno: ${escapeHtml(formatMinutes(userData.minutes))} h</span>
|
||||||
</div>
|
</div>
|
||||||
${leaveHtml}
|
|
||||||
<table>
|
<table>
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
<th style="width:70px">Datum</th>
|
<th style="width:55px">Datum</th>
|
||||||
<th style="width:70px">Typ</th>
|
<th style="width:55px">Den</th>
|
||||||
<th class="text-center" style="width:70px">Příchod</th>
|
<th style="width:60px">Typ</th>
|
||||||
<th class="text-center" style="width:90px">Pauza</th>
|
<th class="text-center" style="width:55px">Příchod</th>
|
||||||
<th class="text-center" style="width:70px">Odchod</th>
|
<th class="text-center" style="width:80px">Pauza</th>
|
||||||
<th class="text-center" style="width:80px">Hodiny</th>
|
<th class="text-center" style="width:55px">Odchod</th>
|
||||||
|
<th class="text-center" style="width:85px">Hodiny</th>
|
||||||
<th>Projekty</th>
|
<th>Projekty</th>
|
||||||
<th>Poznámka</th>
|
<th>Poznámka</th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>${recordRows}</tbody>
|
<tbody>${rows.join("")}</tbody>
|
||||||
<tfoot>
|
|
||||||
<tr>
|
|
||||||
<td colspan="6" class="text-right">Odpracováno:</td>
|
|
||||||
<td class="text-center">${escapeHtml(formatMinutes(userData.minutes))} h</td>
|
|
||||||
<td colspan="2"></td>
|
|
||||||
</tr>
|
|
||||||
${fundRow}
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
</table>
|
||||||
|
${summaryHtml}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,7 +608,13 @@ function buildPrintHtml(
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Docházka - ${escapeHtml(pData.month_name)}</title>
|
<title>Docházka - ${escapeHtml(pData.month_name)}</title>
|
||||||
<style>
|
<style>
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
font-size: 11px; line-height: 1.4; color: #000; background: #fff; padding: 15mm;
|
font-size: 11px; line-height: 1.4; color: #000; background: #fff; padding: 15mm;
|
||||||
@@ -407,19 +644,47 @@ function buildPrintHtml(
|
|||||||
.user-section th { background: #333; color: #fff; font-weight: 600; font-size: 10px; text-transform: uppercase; }
|
.user-section th { background: #333; color: #fff; font-weight: 600; font-size: 10px; text-transform: uppercase; }
|
||||||
.user-section td { font-size: 10px; }
|
.user-section td { font-size: 10px; }
|
||||||
.user-section tr:nth-child(even) { background: #f9f9f9; }
|
.user-section tr:nth-child(even) { background: #f9f9f9; }
|
||||||
|
/* Weekend and holiday row highlighting — must come after the
|
||||||
|
:nth-child(even) rule and use higher specificity (tr.X td) to win. */
|
||||||
|
.user-section tr.weekend td { background: #fde68a; }
|
||||||
|
.user-section tr.holiday td { background: #bbf7d0; }
|
||||||
|
.user-section tr.weekend.holiday td { background: #fcd34d; }
|
||||||
|
.user-section tr.vacation td { background: #bfdbfe; }
|
||||||
.text-center { text-align: center; }
|
.text-center { text-align: center; }
|
||||||
.text-right { text-align: right; }
|
.text-right { text-align: right; }
|
||||||
.user-section tfoot td { background: #eee; font-weight: 600; }
|
|
||||||
.leave-badge { display: inline-block; padding: 2px 6px; border-radius: 3px; font-size: 9px; font-weight: 500; }
|
.leave-badge { display: inline-block; padding: 2px 6px; border-radius: 3px; font-size: 9px; font-weight: 500; }
|
||||||
.badge-vacation { background: #dbeafe; color: #1d4ed8; }
|
.badge-vacation { background: #dbeafe; color: #1d4ed8; }
|
||||||
.badge-sick { background: #fee2e2; color: #dc2626; }
|
.badge-sick { background: #fee2e2; color: #dc2626; }
|
||||||
.badge-holiday { background: #dcfce7; color: #16a34a; }
|
.badge-holiday { background: #dcfce7; color: #16a34a; }
|
||||||
.badge-unpaid { background: #f3f4f6; color: #6b7280; }
|
.badge-unpaid { background: #f3f4f6; color: #6b7280; }
|
||||||
.badge-overtime { background: #fef3c7; color: #d97706; }
|
.badge-overtime { background: #fef3c7; color: #d97706; }
|
||||||
.leave-summary {
|
.user-summary {
|
||||||
margin-top: 10px; padding: 8px 15px; background: #f9f9f9;
|
margin-top: 10px; padding: 10px 15px;
|
||||||
border: 1px solid #ddd; font-size: 10px;
|
background: #f9f9f9; border: 1px solid #ddd; font-size: 10px;
|
||||||
}
|
}
|
||||||
|
.user-summary .summary-row {
|
||||||
|
display: flex; justify-content: space-between; padding: 2px 0;
|
||||||
|
}
|
||||||
|
.user-summary .summary-label { color: #555; }
|
||||||
|
.user-summary .summary-value { font-weight: 600; }
|
||||||
|
.user-summary .summary-fund {
|
||||||
|
border-top: 1px solid #ddd; margin-top: 4px; padding-top: 6px;
|
||||||
|
}
|
||||||
|
.legend {
|
||||||
|
display: flex; flex-wrap: wrap; gap: 14px;
|
||||||
|
margin-top: 8px; padding-top: 6px;
|
||||||
|
border-top: 1px dashed #ddd;
|
||||||
|
font-size: 9px; color: #555;
|
||||||
|
}
|
||||||
|
.legend-item { display: inline-flex; align-items: center; gap: 5px; }
|
||||||
|
.legend-swatch {
|
||||||
|
display: inline-block; width: 12px; height: 12px;
|
||||||
|
border: 1px solid #333; vertical-align: middle;
|
||||||
|
}
|
||||||
|
.legend-swatch.weekend { background: #fde68a; }
|
||||||
|
.legend-swatch.holiday { background: #bbf7d0; }
|
||||||
|
.legend-swatch.weekend-holiday { background: #fcd34d; }
|
||||||
|
.legend-swatch.vacation { background: #bfdbfe; }
|
||||||
.print-wrapper-table { width: 100%; border-collapse: collapse; border: none; }
|
.print-wrapper-table { width: 100%; border-collapse: collapse; border: none; }
|
||||||
.print-wrapper-table > thead > tr > td,
|
.print-wrapper-table > thead > tr > td,
|
||||||
.print-wrapper-table > tbody > tr > td { padding: 0; border: none; background: none; }
|
.print-wrapper-table > tbody > tr > td { padding: 0; border: none; background: none; }
|
||||||
@@ -664,7 +929,15 @@ export default function useAttendanceAdmin({ alert }: AlertContext) {
|
|||||||
formData: ShiftFormData,
|
formData: ShiftFormData,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const totalWork = calcFormWorkMinutes(formData);
|
const totalWork = calcFormWorkMinutes(formData);
|
||||||
const totalProject = calcProjectMinutesTotal(logs);
|
const totalProject = calcProjectMinutesTotal(
|
||||||
|
logs.map((l) => ({
|
||||||
|
...l,
|
||||||
|
project_id:
|
||||||
|
l.project_id !== "" && l.project_id != null
|
||||||
|
? Number(l.project_id)
|
||||||
|
: undefined,
|
||||||
|
})),
|
||||||
|
);
|
||||||
if (totalWork > 0 && totalProject !== totalWork) {
|
if (totalWork > 0 && totalProject !== totalWork) {
|
||||||
const wH = Math.floor(totalWork / 60);
|
const wH = Math.floor(totalWork / 60);
|
||||||
const wM = totalWork % 60;
|
const wM = totalWork % 60;
|
||||||
@@ -869,7 +1142,7 @@ export default function useAttendanceAdmin({ alert }: AlertContext) {
|
|||||||
const userName = record.users
|
const userName = record.users
|
||||||
? `${record.users.first_name} ${record.users.last_name}`.trim() ||
|
? `${record.users.first_name} ${record.users.last_name}`.trim() ||
|
||||||
record.users.username
|
record.users.username
|
||||||
: ((record as Record<string, unknown>).user_name as string) ||
|
: ((record as unknown as Record<string, unknown>).user_name as string) ||
|
||||||
`User #${record.user_id}`;
|
`User #${record.user_id}`;
|
||||||
const enriched = { ...record, user_name: userName };
|
const enriched = { ...record, user_name: userName };
|
||||||
setEditingRecord(enriched);
|
setEditingRecord(enriched);
|
||||||
|
|||||||
24
src/admin/hooks/useReducedMotion.ts
Normal file
24
src/admin/hooks/useReducedMotion.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when the user has expressed a preference for reduced motion
|
||||||
|
* (OS-level setting: `prefers-reduced-motion: reduce`).
|
||||||
|
*
|
||||||
|
* SSR-safe: starts as `false` (the default state), so the first render uses
|
||||||
|
* the normal animation. The effect then reconciles with the live matchMedia
|
||||||
|
* state on the client and re-renders if needed.
|
||||||
|
*/
|
||||||
|
export default function useReducedMotion(): boolean {
|
||||||
|
const [reduced, setReduced] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === "undefined" || !window.matchMedia) return;
|
||||||
|
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||||||
|
setReduced(mq.matches);
|
||||||
|
const onChange = () => setReduced(mq.matches);
|
||||||
|
mq.addEventListener("change", onChange);
|
||||||
|
return () => mq.removeEventListener("change", onChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return reduced;
|
||||||
|
}
|
||||||
@@ -34,10 +34,11 @@ export interface InvoiceStats {
|
|||||||
export interface InvoiceItem {
|
export interface InvoiceItem {
|
||||||
id?: number;
|
id?: number;
|
||||||
description: string;
|
description: string;
|
||||||
item_description: string;
|
item_description?: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
unit: string;
|
unit: string;
|
||||||
unit_price: number;
|
unit_price: number;
|
||||||
|
vat_rate?: number;
|
||||||
is_included_in_total: boolean;
|
is_included_in_total: boolean;
|
||||||
position?: number;
|
position?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
113
src/admin/lib/queries/mutations.ts
Normal file
113
src/admin/lib/queries/mutations.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import {
|
||||||
|
useMutation,
|
||||||
|
useQueryClient,
|
||||||
|
type UseMutationOptions,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
|
import apiFetch from "../../utils/api";
|
||||||
|
|
||||||
|
export interface ApiError extends Error {
|
||||||
|
status?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HttpMethod = "POST" | "PUT" | "PATCH" | "DELETE";
|
||||||
|
|
||||||
|
interface ApiResponseBody<T> {
|
||||||
|
success: boolean;
|
||||||
|
data?: T;
|
||||||
|
error?: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performMutation<TIn, TOut>(opts: {
|
||||||
|
url: string | ((input: TIn) => string);
|
||||||
|
method: HttpMethod | ((input: TIn) => HttpMethod);
|
||||||
|
body?: TIn;
|
||||||
|
}): Promise<TOut> {
|
||||||
|
const url =
|
||||||
|
typeof opts.url === "function" ? opts.url(opts.body as TIn) : opts.url;
|
||||||
|
const method =
|
||||||
|
typeof opts.method === "function"
|
||||||
|
? opts.method(opts.body as TIn)
|
||||||
|
: opts.method;
|
||||||
|
const response = await apiFetch(url, {
|
||||||
|
method,
|
||||||
|
headers:
|
||||||
|
opts.body !== undefined
|
||||||
|
? { "Content-Type": "application/json" }
|
||||||
|
: undefined,
|
||||||
|
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
const err: ApiError = new Error("Unauthorized");
|
||||||
|
err.status = 401;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: ApiResponseBody<TOut>;
|
||||||
|
try {
|
||||||
|
result = (await response.json()) as ApiResponseBody<TOut>;
|
||||||
|
} catch {
|
||||||
|
throw new Error("Invalid JSON response");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok || !result.success) {
|
||||||
|
const err: ApiError = new Error(
|
||||||
|
result.error || `Request failed (${response.status})`,
|
||||||
|
);
|
||||||
|
err.status = response.status;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data as TOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiMutationOptions<TIn, TOut> {
|
||||||
|
url: string | ((input: TIn) => string);
|
||||||
|
method: HttpMethod | ((input: TIn) => HttpMethod);
|
||||||
|
/** Query-key prefixes to invalidate on success. Broad per CLAUDE.md. */
|
||||||
|
invalidate?: readonly string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook that wraps `useMutation` with `apiFetch` and broad-invalidation.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* const createCategory = useApiMutation<CategoryForm, WarehouseCategory>({
|
||||||
|
* url: "/api/admin/warehouse/categories",
|
||||||
|
* method: "POST",
|
||||||
|
* invalidate: ["warehouse"],
|
||||||
|
* });
|
||||||
|
* createCategory.mutate(form, {
|
||||||
|
* onSuccess: () => { ... },
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* The returned `data` from a successful mutation is `TOut`. Errors are thrown
|
||||||
|
* as `ApiError` (with `.status` attached) so callers can branch on HTTP code.
|
||||||
|
*/
|
||||||
|
export function useApiMutation<TIn, TOut>(
|
||||||
|
opts: ApiMutationOptions<TIn, TOut> &
|
||||||
|
Omit<UseMutationOptions<TOut, ApiError, TIn>, "mutationFn">,
|
||||||
|
) {
|
||||||
|
const { url, method, invalidate, onSuccess, ...rest } = opts;
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<TOut, ApiError, TIn, unknown>({
|
||||||
|
mutationFn: (input: TIn) =>
|
||||||
|
performMutation<TIn, TOut>({ url, method, body: input }),
|
||||||
|
...rest,
|
||||||
|
onSuccess: (data, variables, onMutateResult, context) => {
|
||||||
|
if (invalidate) {
|
||||||
|
for (const key of invalidate) {
|
||||||
|
queryClient.invalidateQueries({ queryKey: [key] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Forward to user-provided onSuccess with the 4-arg signature expected by TanStack.
|
||||||
|
(
|
||||||
|
onSuccess as
|
||||||
|
| ((d: TOut, v: TIn, r: unknown, c: unknown) => void)
|
||||||
|
| undefined
|
||||||
|
)?.(data, variables, onMutateResult, context);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -145,6 +145,9 @@ export const offerDetailOptions = (id: string | undefined) =>
|
|||||||
queryKey: ["offers", id],
|
queryKey: ["offers", id],
|
||||||
queryFn: () => jsonQuery<OfferDetailData>(`/api/admin/offers/${id}`),
|
queryFn: () => jsonQuery<OfferDetailData>(`/api/admin/offers/${id}`),
|
||||||
enabled: !!id,
|
enabled: !!id,
|
||||||
|
// 404s (deleted/missing offers) are not transient. Retrying just spams
|
||||||
|
// GETs and fires the detail-page redirect toast repeatedly.
|
||||||
|
retry: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const offerNextNumberOptions = () =>
|
export const offerNextNumberOptions = () =>
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ export interface ProjectData {
|
|||||||
project_notes?: ProjectNote[];
|
project_notes?: ProjectNote[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Project {
|
||||||
|
id: number;
|
||||||
|
project_number: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const projectListOptions = (filters: {
|
export const projectListOptions = (filters: {
|
||||||
search?: string;
|
search?: string;
|
||||||
sort?: string;
|
sort?: string;
|
||||||
@@ -45,7 +51,9 @@ export const projectListOptions = (filters: {
|
|||||||
if (filters.page) params.set("page", String(filters.page));
|
if (filters.page) params.set("page", String(filters.page));
|
||||||
if (filters.perPage) params.set("per_page", String(filters.perPage));
|
if (filters.perPage) params.set("per_page", String(filters.perPage));
|
||||||
const qs = params.toString();
|
const qs = params.toString();
|
||||||
return paginatedJsonQuery(`/api/admin/projects${qs ? `?${qs}` : ""}`);
|
return paginatedJsonQuery<Project>(
|
||||||
|
`/api/admin/projects${qs ? `?${qs}` : ""}`,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,12 @@ export interface CompanySettingsData {
|
|||||||
offer_number_pattern?: string;
|
offer_number_pattern?: string;
|
||||||
order_number_pattern?: string;
|
order_number_pattern?: string;
|
||||||
invoice_number_pattern?: string;
|
invoice_number_pattern?: string;
|
||||||
|
warehouse_receipt_prefix?: string;
|
||||||
|
warehouse_receipt_number_pattern?: string;
|
||||||
|
warehouse_issue_prefix?: string;
|
||||||
|
warehouse_issue_number_pattern?: string;
|
||||||
|
warehouse_inventory_prefix?: string;
|
||||||
|
warehouse_inventory_number_pattern?: string;
|
||||||
|
|
||||||
// Misc
|
// Misc
|
||||||
app_version?: string;
|
app_version?: string;
|
||||||
|
|||||||
@@ -37,11 +37,12 @@ export interface WarehouseReceipt {
|
|||||||
first_name: string;
|
first_name: string;
|
||||||
last_name: string;
|
last_name: string;
|
||||||
} | null;
|
} | null;
|
||||||
lines?: WarehouseReceiptLine[];
|
_count?: { items: number };
|
||||||
|
items?: WarehouseReceiptItem[];
|
||||||
attachments?: WarehouseReceiptAttachment[];
|
attachments?: WarehouseReceiptAttachment[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WarehouseReceiptLine {
|
export interface WarehouseReceiptItem {
|
||||||
id: number;
|
id: number;
|
||||||
receipt_id: number;
|
receipt_id: number;
|
||||||
item_id: number;
|
item_id: number;
|
||||||
@@ -74,10 +75,11 @@ export interface WarehouseIssue {
|
|||||||
modified_at: string | null;
|
modified_at: string | null;
|
||||||
project?: { id: number; name: string; project_number: string } | null;
|
project?: { id: number; name: string; project_number: string } | null;
|
||||||
issued_by_user?: { id: number; first_name: string; last_name: string } | null;
|
issued_by_user?: { id: number; first_name: string; last_name: string } | null;
|
||||||
lines?: WarehouseIssueLine[];
|
_count?: { items: number };
|
||||||
|
items?: WarehouseIssueItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WarehouseIssueLine {
|
export interface WarehouseIssueItem {
|
||||||
id: number;
|
id: number;
|
||||||
issue_id: number;
|
issue_id: number;
|
||||||
item_id: number;
|
item_id: number;
|
||||||
@@ -124,10 +126,10 @@ export interface WarehouseInventorySession {
|
|||||||
status: "DRAFT" | "CONFIRMED";
|
status: "DRAFT" | "CONFIRMED";
|
||||||
created_at: string;
|
created_at: string;
|
||||||
modified_at: string | null;
|
modified_at: string | null;
|
||||||
lines?: WarehouseInventoryLine[];
|
items?: WarehouseInventoryItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WarehouseInventoryLine {
|
export interface WarehouseInventoryItem {
|
||||||
id: number;
|
id: number;
|
||||||
session_id: number;
|
session_id: number;
|
||||||
item_id: number;
|
item_id: number;
|
||||||
@@ -168,6 +170,16 @@ export interface WarehouseSupplier {
|
|||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MovementLogRow {
|
||||||
|
date: string;
|
||||||
|
type: string;
|
||||||
|
document_number: string;
|
||||||
|
item_name: string;
|
||||||
|
quantity: number;
|
||||||
|
unit_price: number;
|
||||||
|
supplier_or_project: string;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Query Options ---
|
// --- Query Options ---
|
||||||
|
|
||||||
export const warehouseItemListOptions = (filters: {
|
export const warehouseItemListOptions = (filters: {
|
||||||
@@ -196,11 +208,14 @@ export const warehouseItemListOptions = (filters: {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const warehouseItemDetailOptions = (id: string | undefined) =>
|
export const warehouseItemDetailOptions = (
|
||||||
|
id: string | undefined,
|
||||||
|
enabled?: boolean,
|
||||||
|
) =>
|
||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ["warehouse", "items", id],
|
queryKey: ["warehouse", "items", id],
|
||||||
queryFn: () => jsonQuery<WarehouseItem>(`/api/admin/warehouse/items/${id}`),
|
queryFn: () => jsonQuery<WarehouseItem>(`/api/admin/warehouse/items/${id}`),
|
||||||
enabled: !!id,
|
enabled: enabled ?? !!id,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const warehouseCategoryListOptions = () =>
|
export const warehouseCategoryListOptions = () =>
|
||||||
@@ -229,11 +244,17 @@ export const warehouseSupplierListOptions = (filters: {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const warehouseLocationListOptions = () =>
|
export const warehouseLocationListOptions = (filters?: {
|
||||||
|
active?: "true" | "false" | "all";
|
||||||
|
}) =>
|
||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ["warehouse", "locations"],
|
queryKey: ["warehouse", "locations", filters ?? { active: "true" }],
|
||||||
queryFn: () =>
|
queryFn: () => {
|
||||||
jsonQuery<WarehouseLocation[]>("/api/admin/warehouse/locations"),
|
const active = filters?.active ?? "true";
|
||||||
|
return jsonQuery<WarehouseLocation[]>(
|
||||||
|
`/api/admin/warehouse/locations?active=${active}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const warehouseReceiptListOptions = (filters: {
|
export const warehouseReceiptListOptions = (filters: {
|
||||||
@@ -346,7 +367,7 @@ export const warehouseInventoryListOptions = (filters: {
|
|||||||
if (filters.status) params.set("status", filters.status);
|
if (filters.status) params.set("status", filters.status);
|
||||||
const qs = params.toString();
|
const qs = params.toString();
|
||||||
return paginatedJsonQuery<WarehouseInventorySession>(
|
return paginatedJsonQuery<WarehouseInventorySession>(
|
||||||
`/api/admin/warehouse/inventory${qs ? `?${qs}` : ""}`,
|
`/api/admin/warehouse/inventory-sessions${qs ? `?${qs}` : ""}`,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -356,7 +377,7 @@ export const warehouseInventoryDetailOptions = (id: string | undefined) =>
|
|||||||
queryKey: ["warehouse", "inventory", id],
|
queryKey: ["warehouse", "inventory", id],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
jsonQuery<WarehouseInventorySession>(
|
jsonQuery<WarehouseInventorySession>(
|
||||||
`/api/admin/warehouse/inventory/${id}`,
|
`/api/admin/warehouse/inventory-sessions/${id}`,
|
||||||
),
|
),
|
||||||
enabled: !!id,
|
enabled: !!id,
|
||||||
});
|
});
|
||||||
@@ -372,7 +393,7 @@ export const warehouseStockStatusOptions = (filters?: {
|
|||||||
params.set("category_id", String(filters.category_id));
|
params.set("category_id", String(filters.category_id));
|
||||||
const qs = params.toString();
|
const qs = params.toString();
|
||||||
return jsonQuery<WarehouseItem[]>(
|
return jsonQuery<WarehouseItem[]>(
|
||||||
`/api/admin/warehouse/stock-status${qs ? `?${qs}` : ""}`,
|
`/api/admin/warehouse/reports/stock-status${qs ? `?${qs}` : ""}`,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -381,5 +402,29 @@ export const warehouseBelowMinimumOptions = () =>
|
|||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ["warehouse", "below-minimum"],
|
queryKey: ["warehouse", "below-minimum"],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
jsonQuery<WarehouseItem[]>("/api/admin/warehouse/below-minimum"),
|
jsonQuery<WarehouseItem[]>("/api/admin/warehouse/reports/below-minimum"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const warehouseMovementLogOptions = (filters?: {
|
||||||
|
limit?: number;
|
||||||
|
type?: string;
|
||||||
|
project_id?: number;
|
||||||
|
date_from?: string;
|
||||||
|
date_to?: string;
|
||||||
|
}) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["warehouse", "movement-log", filters],
|
||||||
|
queryFn: () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filters?.limit) params.set("limit", String(filters.limit));
|
||||||
|
if (filters?.type) params.set("type", filters.type);
|
||||||
|
if (filters?.project_id)
|
||||||
|
params.set("project_id", String(filters.project_id));
|
||||||
|
if (filters?.date_from) params.set("date_from", filters.date_from);
|
||||||
|
if (filters?.date_to) params.set("date_to", filters.date_to);
|
||||||
|
const qs = params.toString();
|
||||||
|
return jsonQuery<MovementLogRow[]>(
|
||||||
|
`/api/admin/warehouse/reports/movement-log${qs ? `?${qs}` : ""}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
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";
|
||||||
@@ -16,8 +16,8 @@ import FormField from "../components/FormField";
|
|||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
import { jsonQuery } from "../lib/apiAdapter";
|
import { jsonQuery } from "../lib/apiAdapter";
|
||||||
import { Skeleton } from "boneyard-js/react";
|
import { useApiMutation } from "../lib/queries/mutations";
|
||||||
import AttendanceFixture from "../fixtures/AttendanceFixture";
|
import { czechPlural } from "../utils/formatters";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
@@ -65,7 +65,6 @@ interface MonthlyFund {
|
|||||||
leave_hours: number;
|
leave_hours: number;
|
||||||
vacation_hours: number;
|
vacation_hours: number;
|
||||||
sick_hours: number;
|
sick_hours: number;
|
||||||
holiday_hours: number;
|
|
||||||
unpaid_hours: number;
|
unpaid_hours: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,12 +78,6 @@ interface AttendanceData {
|
|||||||
active_project_id: number | null;
|
active_project_id: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function pluralizeDays(n: number) {
|
|
||||||
if (n === 1) return "den";
|
|
||||||
if (n >= 2 && n <= 4) return "dny";
|
|
||||||
return "dnů";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFundBarBackground(fund: MonthlyFund) {
|
function getFundBarBackground(fund: MonthlyFund) {
|
||||||
if (fund.overtime > 0)
|
if (fund.overtime > 0)
|
||||||
return "linear-gradient(135deg, var(--warning), #d97706)";
|
return "linear-gradient(135deg, var(--warning), #d97706)";
|
||||||
@@ -96,7 +89,6 @@ function getFundBarBackground(fund: MonthlyFund) {
|
|||||||
export default function Attendance() {
|
export default function Attendance() {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const statusQuery = useQuery({
|
const statusQuery = useQuery({
|
||||||
queryKey: ["attendance", "status"],
|
queryKey: ["attendance", "status"],
|
||||||
@@ -109,6 +101,41 @@ export default function Attendance() {
|
|||||||
jsonQuery<Project[]>("/api/admin/attendance?action=projects"),
|
jsonQuery<Project[]>("/api/admin/attendance?action=projects"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const punchMutation = useApiMutation<
|
||||||
|
Record<string, unknown>,
|
||||||
|
{ message?: string }
|
||||||
|
>({
|
||||||
|
url: () => `${API_BASE}/attendance`,
|
||||||
|
method: () => "POST",
|
||||||
|
invalidate: ["attendance"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const notesMutation = useApiMutation<{ notes: string }, { message?: string }>(
|
||||||
|
{
|
||||||
|
url: () => `${API_BASE}/attendance/notes`,
|
||||||
|
method: () => "POST",
|
||||||
|
invalidate: ["attendance"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const switchProjectMutation = useApiMutation<
|
||||||
|
{ project_id: number | null },
|
||||||
|
{ message?: string }
|
||||||
|
>({
|
||||||
|
url: () => `${API_BASE}/attendance/switch-project`,
|
||||||
|
method: () => "POST",
|
||||||
|
invalidate: ["attendance"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const leaveRequestMutation = useApiMutation<
|
||||||
|
{ leave_type: string; date_from: string; date_to: string; notes: string },
|
||||||
|
{ message?: string }
|
||||||
|
>({
|
||||||
|
url: () => `${API_BASE}/leave-requests`,
|
||||||
|
method: () => "POST",
|
||||||
|
invalidate: ["attendance", "leave-requests", "users"],
|
||||||
|
});
|
||||||
|
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [isLeaveModalOpen, setIsLeaveModalOpen] = useState(false);
|
const [isLeaveModalOpen, setIsLeaveModalOpen] = useState(false);
|
||||||
const [leaveForm, setLeaveForm] = useState({
|
const [leaveForm, setLeaveForm] = useState({
|
||||||
@@ -232,55 +259,29 @@ export default function Attendance() {
|
|||||||
gpsData: Record<string, unknown> = {},
|
gpsData: Record<string, unknown> = {},
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(`${API_BASE}/attendance`, {
|
const result = await punchMutation.mutateAsync({
|
||||||
method: "POST",
|
punch_action: action,
|
||||||
headers: { "Content-Type": "application/json" },
|
...gpsData,
|
||||||
body: JSON.stringify({ punch_action: action, ...gpsData }),
|
|
||||||
});
|
});
|
||||||
if (response.status === 401) return;
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: ["attendance"],
|
|
||||||
});
|
|
||||||
punchTimeoutRef.current = setTimeout(() => {
|
punchTimeoutRef.current = setTimeout(() => {
|
||||||
alert.success(result.data?.message || result.message || "Uloženo");
|
alert.success(result?.message || "Uloženo");
|
||||||
}, 300);
|
}, 300);
|
||||||
} else {
|
} catch (e) {
|
||||||
alert.error(result.error);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
alert.error("Chyba připojení");
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBreak = async () => {
|
const handleBreak = async () => {
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(`${API_BASE}/attendance`, {
|
const result = await punchMutation.mutateAsync({
|
||||||
method: "POST",
|
punch_action: "break_start",
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ punch_action: "break_start" }),
|
|
||||||
});
|
});
|
||||||
if (response.status === 401) return;
|
alert.success(result?.message || "Přestávka zaznamenána");
|
||||||
|
} catch (e) {
|
||||||
const result = await response.json();
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
if (result.success) {
|
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: ["attendance"],
|
|
||||||
});
|
|
||||||
alert.success(
|
|
||||||
result.data?.message || result.message || "Přestávka zaznamenána",
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
alert.error(result.error);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
@@ -288,48 +289,22 @@ export default function Attendance() {
|
|||||||
|
|
||||||
const handleSaveNotes = async () => {
|
const handleSaveNotes = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(`${API_BASE}/attendance/notes`, {
|
await notesMutation.mutateAsync({ notes });
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ notes }),
|
|
||||||
});
|
|
||||||
if (response.status === 401) return;
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
await queryClient.invalidateQueries({ queryKey: ["attendance"] });
|
|
||||||
alert.success("Poznámka byla uložena");
|
alert.success("Poznámka byla uložena");
|
||||||
} else {
|
} catch (e) {
|
||||||
alert.error(result.error);
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSwitchProject = async (newProjectId: string | null) => {
|
const handleSwitchProject = async (newProjectId: string | null) => {
|
||||||
setSwitchingProject(true);
|
setSwitchingProject(true);
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(`${API_BASE}/attendance/switch-project`, {
|
const result = await switchProjectMutation.mutateAsync({
|
||||||
method: "POST",
|
project_id: newProjectId ? Number(newProjectId) : null,
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ project_id: newProjectId || null }),
|
|
||||||
});
|
});
|
||||||
if (response.status === 401) return;
|
alert.success(result?.message || "Projekt přepnut");
|
||||||
|
} catch (e) {
|
||||||
const result = await response.json();
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
if (result.success) {
|
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: ["attendance"],
|
|
||||||
});
|
|
||||||
alert.success(
|
|
||||||
result.data?.message || result.message || "Projekt přepnut",
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
alert.error(result.error);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
} finally {
|
||||||
setSwitchingProject(false);
|
setSwitchingProject(false);
|
||||||
}
|
}
|
||||||
@@ -353,56 +328,33 @@ export default function Attendance() {
|
|||||||
const handleRequestSubmit = async () => {
|
const handleRequestSubmit = async () => {
|
||||||
setRequestSubmitting(true);
|
setRequestSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(`${API_BASE}/leave-requests`, {
|
const result = await leaveRequestMutation.mutateAsync(leaveForm);
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(leaveForm),
|
|
||||||
});
|
|
||||||
if (response.status === 401) return;
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setIsLeaveModalOpen(false);
|
setIsLeaveModalOpen(false);
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: ["attendance"],
|
|
||||||
});
|
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: ["leave-requests"],
|
|
||||||
});
|
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: ["leave"],
|
|
||||||
});
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
alert.success(
|
alert.success(result?.message || "Žádost odeslána");
|
||||||
result.data?.message || result.message || "Žádost odeslána",
|
|
||||||
);
|
|
||||||
setLeaveForm({
|
setLeaveForm({
|
||||||
leave_type: "vacation",
|
leave_type: "vacation",
|
||||||
date_from: new Date().toISOString().split("T")[0],
|
date_from: new Date().toISOString().split("T")[0],
|
||||||
date_to: new Date().toISOString().split("T")[0],
|
date_to: new Date().toISOString().split("T")[0],
|
||||||
notes: "",
|
notes: "",
|
||||||
});
|
});
|
||||||
} else {
|
} catch (e) {
|
||||||
alert.error(result.error);
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
} finally {
|
||||||
setRequestSubmitting(false);
|
setRequestSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!statusQuery.data) {
|
if (statusQuery.isPending) {
|
||||||
return (
|
return (
|
||||||
<Skeleton
|
<div className="admin-loading">
|
||||||
name="attendance"
|
<div className="admin-spinner" />
|
||||||
loading={statusQuery.isPending}
|
</div>
|
||||||
fixture={<AttendanceFixture />}
|
|
||||||
>
|
|
||||||
<div />
|
|
||||||
</Skeleton>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (!statusQuery.data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
ongoing_shift: ongoingShift,
|
ongoing_shift: ongoingShift,
|
||||||
@@ -671,7 +623,13 @@ export default function Attendance() {
|
|||||||
{formatTime(shift.departure_time)}
|
{formatTime(shift.departure_time)}
|
||||||
</td>
|
</td>
|
||||||
<td className="admin-mono">
|
<td className="admin-mono">
|
||||||
{formatMinutes(calculateWorkMinutes(shift), true)}
|
{formatMinutes(
|
||||||
|
calculateWorkMinutes({
|
||||||
|
...shift,
|
||||||
|
notes: shift.notes ?? undefined,
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
{projects.length > 0 && (
|
{projects.length > 0 && (
|
||||||
<td>
|
<td>
|
||||||
@@ -741,7 +699,7 @@ export default function Attendance() {
|
|||||||
{vacationDaysRemaining}
|
{vacationDaysRemaining}
|
||||||
</span>
|
</span>
|
||||||
<span className="attendance-balance-unit">
|
<span className="attendance-balance-unit">
|
||||||
{pluralizeDays(vacationDaysRemaining)}
|
{czechPlural(vacationDaysRemaining, "den", "dny", "dnů")}
|
||||||
{vacationHoursRemaining > 0 && ` ${vacationHoursRemaining}h`}
|
{vacationHoursRemaining > 0 && ` ${vacationHoursRemaining}h`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -831,8 +789,6 @@ export default function Attendance() {
|
|||||||
` + dovolená ${data.monthly_fund.vacation_hours}h`}
|
` + dovolená ${data.monthly_fund.vacation_hours}h`}
|
||||||
{data.monthly_fund.sick_hours > 0 &&
|
{data.monthly_fund.sick_hours > 0 &&
|
||||||
` + nemoc ${data.monthly_fund.sick_hours}h`}
|
` + nemoc ${data.monthly_fund.sick_hours}h`}
|
||||||
{data.monthly_fund.holiday_hours > 0 &&
|
|
||||||
` + svátek ${data.monthly_fund.holiday_hours}h`}
|
|
||||||
{data.monthly_fund.unpaid_hours > 0 &&
|
{data.monthly_fund.unpaid_hours > 0 &&
|
||||||
` + neplacené ${data.monthly_fund.unpaid_hours}h`}
|
` + neplacené ${data.monthly_fund.unpaid_hours}h`}
|
||||||
)
|
)
|
||||||
@@ -1069,15 +1025,15 @@ export default function Attendance() {
|
|||||||
leaveForm.date_to,
|
leaveForm.date_to,
|
||||||
)}
|
)}
|
||||||
</strong>{" "}
|
</strong>{" "}
|
||||||
{(() => {
|
{czechPlural(
|
||||||
const d = calculateBusinessDays(
|
calculateBusinessDays(
|
||||||
leaveForm.date_from,
|
leaveForm.date_from,
|
||||||
leaveForm.date_to,
|
leaveForm.date_to,
|
||||||
);
|
),
|
||||||
if (d === 1) return "pracovní den";
|
"pracovní den",
|
||||||
if (d >= 2 && d <= 4) return "pracovní dny";
|
"pracovní dny",
|
||||||
return "pracovních dnů";
|
"pracovních dnů",
|
||||||
})()}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-muted">
|
<span className="text-muted">
|
||||||
{calculateBusinessDays(
|
{calculateBusinessDays(
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ import AttendanceShiftTable from "../components/AttendanceShiftTable";
|
|||||||
import useModalLock from "../hooks/useModalLock";
|
import useModalLock from "../hooks/useModalLock";
|
||||||
import useAttendanceAdmin from "../hooks/useAttendanceAdmin";
|
import useAttendanceAdmin from "../hooks/useAttendanceAdmin";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
import { formatMinutes } from "../utils/attendanceHelpers";
|
import { formatMinutes, formatHoursDecimal } from "../utils/attendanceHelpers";
|
||||||
import { Skeleton } from "boneyard-js/react";
|
|
||||||
import AttendanceAdminFixture from "../fixtures/AttendanceAdminFixture";
|
|
||||||
|
|
||||||
interface UserTotalData {
|
interface UserTotalData {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -20,20 +18,34 @@ interface UserTotalData {
|
|||||||
working: boolean;
|
working: boolean;
|
||||||
vacation_hours: number;
|
vacation_hours: number;
|
||||||
sick_hours: number;
|
sick_hours: number;
|
||||||
holiday_hours: number;
|
|
||||||
unpaid_hours: number;
|
unpaid_hours: number;
|
||||||
fund: number | null;
|
fund: number | null;
|
||||||
worked_hours: number;
|
worked_hours: number;
|
||||||
covered: number;
|
covered: number;
|
||||||
missing: number;
|
missing: number;
|
||||||
overtime: number;
|
overtime: number;
|
||||||
|
// mzda-style summary fields (set by computeUserTotals in useAttendanceAdmin)
|
||||||
|
odpracovano?: number;
|
||||||
|
vcetne_svatku?: number;
|
||||||
|
prescas?: number;
|
||||||
|
svatek?: number;
|
||||||
|
worked_holiday_hours?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFundBarBackground(data: UserTotalData) {
|
function getFundBarBackground(data: UserTotalData) {
|
||||||
if (data.overtime > 0)
|
// fondUsed mirrors the Fond "used" line in the IIFE below:
|
||||||
return "linear-gradient(135deg, var(--warning), #d97706)";
|
// real_worked + vacation + worked_holiday + free_holiday
|
||||||
if (data.covered >= (data.fund ?? 0))
|
// (delta is fondUsed - fund; 0 means exactly at the fund).
|
||||||
return "linear-gradient(135deg, var(--success), #059669)";
|
const realWorked = data.worked_hours_raw ?? data.worked_hours;
|
||||||
|
const fondUsed =
|
||||||
|
realWorked +
|
||||||
|
(data.vacation_hours ?? 0) +
|
||||||
|
(data.worked_holiday_hours ?? 0) +
|
||||||
|
(data.svatek ?? 0);
|
||||||
|
const fundVal = data.fund ?? 0;
|
||||||
|
const delta = fondUsed - fundVal;
|
||||||
|
if (delta > 0) return "linear-gradient(135deg, var(--warning), #d97706)";
|
||||||
|
if (delta >= 0) return "linear-gradient(135deg, var(--success), #059669)";
|
||||||
return "var(--gradient)";
|
return "var(--gradient)";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +101,7 @@ export default function AttendanceAdmin() {
|
|||||||
|
|
||||||
if (!hasPermission("attendance.manage")) return <Forbidden />;
|
if (!hasPermission("attendance.manage")) return <Forbidden />;
|
||||||
|
|
||||||
// Show skeleton only on initial load (no data yet), not on filter changes
|
// Show spinner only on initial load (no data yet), not on filter changes
|
||||||
const isInitialLoad =
|
const isInitialLoad =
|
||||||
loading &&
|
loading &&
|
||||||
data.records.length === 0 &&
|
data.records.length === 0 &&
|
||||||
@@ -97,13 +109,9 @@ export default function AttendanceAdmin() {
|
|||||||
|
|
||||||
if (isInitialLoad) {
|
if (isInitialLoad) {
|
||||||
return (
|
return (
|
||||||
<Skeleton
|
<div className="admin-loading">
|
||||||
name="attendance-admin"
|
<div className="admin-spinner" />
|
||||||
loading={isInitialLoad}
|
</div>
|
||||||
fixture={<AttendanceAdminFixture />}
|
|
||||||
>
|
|
||||||
<div />
|
|
||||||
</Skeleton>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,8 +220,15 @@ export default function AttendanceAdmin() {
|
|||||||
{Object.entries(data.user_totals).map(([uid, userData]) => {
|
{Object.entries(data.user_totals).map(([uid, userData]) => {
|
||||||
const ut = userData as UserTotalData;
|
const ut = userData as UserTotalData;
|
||||||
return (
|
return (
|
||||||
<div key={uid} className="admin-card">
|
<div key={uid} className="admin-card" style={{ display: "flex" }}>
|
||||||
<div className="admin-card-body">
|
<div
|
||||||
|
className="admin-card-body"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="flex-row gap-2 mb-2">
|
<div className="flex-row gap-2 mb-2">
|
||||||
<span style={{ fontWeight: 600 }}>{ut.name}</span>
|
<span style={{ fontWeight: 600 }}>{ut.name}</span>
|
||||||
<span
|
<span
|
||||||
@@ -229,9 +244,11 @@ export default function AttendanceAdmin() {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginTop: "0.5rem",
|
marginTop: "0.5rem",
|
||||||
|
marginBottom: "0.75rem",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexWrap: "wrap",
|
flexWrap: "wrap",
|
||||||
gap: "0.25rem",
|
gap: "0.4rem",
|
||||||
|
minHeight: "2rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{ut.vacation_hours > 0 && (
|
{ut.vacation_hours > 0 && (
|
||||||
@@ -244,9 +261,9 @@ export default function AttendanceAdmin() {
|
|||||||
Nem: {ut.sick_hours}h
|
Nem: {ut.sick_hours}h
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{ut.holiday_hours > 0 && (
|
{ut.svatek !== undefined && ut.svatek > 0 && (
|
||||||
<span className="attendance-leave-badge badge-holiday">
|
<span className="attendance-leave-badge badge-holiday">
|
||||||
Sv: {ut.holiday_hours}h
|
Sv: {Math.round(ut.svatek)}h
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{ut.unpaid_hours > 0 && (
|
{ut.unpaid_hours > 0 && (
|
||||||
@@ -255,8 +272,30 @@ export default function AttendanceAdmin() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{ut.fund !== null && (
|
{ut.fund !== null &&
|
||||||
<div className="mt-2">
|
(() => {
|
||||||
|
// Fond "used" = real_worked + vacation + worked_holiday + free_holiday
|
||||||
|
// (everything the user actually got paid for this month:
|
||||||
|
// raw worked time with sub-day remainders, plus
|
||||||
|
// vacation days, plus hours worked on holidays, plus
|
||||||
|
// the 8h per unworked holiday from the fund).
|
||||||
|
const realWorked = ut.worked_hours_raw ?? ut.worked_hours;
|
||||||
|
const fondUsed =
|
||||||
|
realWorked +
|
||||||
|
(ut.vacation_hours ?? 0) +
|
||||||
|
(ut.worked_holiday_hours ?? 0) +
|
||||||
|
(ut.svatek ?? 0);
|
||||||
|
const fundVal = ut.fund ?? 0;
|
||||||
|
const delta = fondUsed - fundVal;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="kpi-card-footer"
|
||||||
|
style={{
|
||||||
|
marginTop: "auto",
|
||||||
|
paddingTop: "0.75rem",
|
||||||
|
borderTop: "1px solid var(--border-color)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="text-secondary"
|
className="text-secondary"
|
||||||
style={{
|
style={{
|
||||||
@@ -267,16 +306,17 @@ export default function AttendanceAdmin() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
Fond: {ut.worked_hours}h / {ut.fund}h
|
Fond: {formatHoursDecimal(fondUsed * 60)}h /{" "}
|
||||||
|
{formatHoursDecimal(fundVal * 60)}h
|
||||||
</span>
|
</span>
|
||||||
{ut.overtime > 0 && (
|
{delta > 0 && (
|
||||||
<span className="text-warning fw-600">
|
<span className="text-warning fw-600">
|
||||||
+{ut.overtime}h
|
+{formatHoursDecimal(delta * 60)}h
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{ut.overtime <= 0 && ut.missing > 0 && (
|
{delta <= 0 && delta < 0 && (
|
||||||
<span className="text-danger fw-600">
|
<span className="text-danger fw-600">
|
||||||
-{ut.missing}h
|
-{formatHoursDecimal(Math.abs(delta) * 60)}h
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -292,7 +332,10 @@ export default function AttendanceAdmin() {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: "100%",
|
height: "100%",
|
||||||
width: `${Math.min(100, (ut.covered / (ut.fund || 1)) * 100)}%`,
|
width: `${Math.min(
|
||||||
|
100,
|
||||||
|
(fondUsed / (ut.fund || 1)) * 100,
|
||||||
|
)}%`,
|
||||||
background: getFundBarBackground(ut),
|
background: getFundBarBackground(ut),
|
||||||
borderRadius: "2px",
|
borderRadius: "2px",
|
||||||
transition: "width 0.3s ease",
|
transition: "width 0.3s ease",
|
||||||
@@ -300,19 +343,24 @@ export default function AttendanceAdmin() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
{data.leave_balances[uid] && (
|
})()}
|
||||||
<div
|
<div
|
||||||
className="text-secondary"
|
className="text-secondary"
|
||||||
style={{ marginTop: "0.5rem", fontSize: "0.8rem" }}
|
style={{ marginTop: "0.75rem", fontSize: "0.8rem" }}
|
||||||
>
|
>
|
||||||
|
{data.leave_balances[uid] ? (
|
||||||
|
<>
|
||||||
Zbývá dovolené:{" "}
|
Zbývá dovolené:{" "}
|
||||||
{data.leave_balances[uid].vacation_remaining.toFixed(1)}h
|
{data.leave_balances[uid].vacation_remaining.toFixed(1)}
|
||||||
/ {data.leave_balances[uid].vacation_total}h
|
h / {data.leave_balances[uid].vacation_total}h
|
||||||
</div>
|
</>
|
||||||
|
) : (
|
||||||
|
<span style={{ visibility: "hidden" }}>—</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -374,7 +422,14 @@ export default function AttendanceAdmin() {
|
|||||||
projectList={projectList}
|
projectList={projectList}
|
||||||
users={data.users}
|
users={data.users}
|
||||||
onShiftDateChange={handleCreateShiftDateChange}
|
onShiftDateChange={handleCreateShiftDateChange}
|
||||||
editingRecord={editingRecord}
|
editingRecord={
|
||||||
|
editingRecord
|
||||||
|
? {
|
||||||
|
user_name: editingRecord.user_name ?? "",
|
||||||
|
shift_date: editingRecord.shift_date,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { useState } from "react";
|
|||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import ConfirmModal from "../components/ConfirmModal";
|
import ConfirmModal from "../components/ConfirmModal";
|
||||||
import useModalLock from "../hooks/useModalLock";
|
import FormModal from "../components/FormModal";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
attendanceBalancesOptions,
|
attendanceBalancesOptions,
|
||||||
attendanceWorkFundOptions,
|
attendanceWorkFundOptions,
|
||||||
@@ -20,9 +20,8 @@ import {
|
|||||||
type UserShort,
|
type UserShort,
|
||||||
} from "../lib/queries/attendance";
|
} from "../lib/queries/attendance";
|
||||||
|
|
||||||
import apiFetch from "../utils/api";
|
import { useApiMutation } from "../lib/queries/mutations";
|
||||||
import { Skeleton } from "boneyard-js/react";
|
|
||||||
import AttendanceBalancesFixture from "../fixtures/AttendanceBalancesFixture";
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
const getVacationClass = (remaining: number): string => {
|
const getVacationClass = (remaining: number): string => {
|
||||||
@@ -85,7 +84,6 @@ const getProgressBackground = (
|
|||||||
export default function AttendanceBalances() {
|
export default function AttendanceBalances() {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const [year, setYear] = useState(new Date().getFullYear());
|
const [year, setYear] = useState(new Date().getFullYear());
|
||||||
const { data: balancesData, isPending: balancesPending } = useQuery(
|
const { data: balancesData, isPending: balancesPending } = useQuery(
|
||||||
attendanceBalancesOptions(year),
|
attendanceBalancesOptions(year),
|
||||||
@@ -114,10 +112,37 @@ export default function AttendanceBalances() {
|
|||||||
userName: string;
|
userName: string;
|
||||||
}>({ show: false, userId: null, userName: "" });
|
}>({ show: false, userId: null, userName: "" });
|
||||||
|
|
||||||
useModalLock(showEditModal);
|
|
||||||
|
|
||||||
if (!hasPermission("attendance.balances")) return <Forbidden />;
|
if (!hasPermission("attendance.balances")) return <Forbidden />;
|
||||||
|
|
||||||
|
const editMutation = useApiMutation<
|
||||||
|
{
|
||||||
|
user_id: string;
|
||||||
|
year: number;
|
||||||
|
action_type: "edit";
|
||||||
|
vacation_total: number;
|
||||||
|
vacation_used: number;
|
||||||
|
sick_used: number;
|
||||||
|
},
|
||||||
|
{ message?: string; error?: string }
|
||||||
|
>({
|
||||||
|
url: () => `${API_BASE}/attendance?action=balances`,
|
||||||
|
method: () => "POST",
|
||||||
|
invalidate: ["attendance", "users"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetMutation = useApiMutation<
|
||||||
|
{ user_id: string; year: number; action_type: "reset" },
|
||||||
|
{ message?: string; error?: string }
|
||||||
|
>({
|
||||||
|
url: () => `${API_BASE}/attendance?action=balances`,
|
||||||
|
method: () => "POST",
|
||||||
|
invalidate: ["attendance", "users"],
|
||||||
|
onSuccess: (data) => {
|
||||||
|
setResetConfirm({ show: false, userId: null, userName: "" });
|
||||||
|
alert.success(data?.message || "Operace dokončena");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const openEditModal = (userId: string, balance: BalanceEntry) => {
|
const openEditModal = (userId: string, balance: BalanceEntry) => {
|
||||||
setEditingUser({ id: userId, name: balance.name });
|
setEditingUser({ id: userId, name: balance.name });
|
||||||
setEditForm({
|
setEditForm({
|
||||||
@@ -131,62 +156,29 @@ export default function AttendanceBalances() {
|
|||||||
const handleEditSubmit = async () => {
|
const handleEditSubmit = async () => {
|
||||||
if (!editingUser) return;
|
if (!editingUser) return;
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(
|
const result = await editMutation.mutateAsync({
|
||||||
`${API_BASE}/attendance?action=balances`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
user_id: editingUser.id,
|
user_id: editingUser.id,
|
||||||
year,
|
year,
|
||||||
action_type: "edit",
|
action_type: "edit",
|
||||||
...editForm,
|
...editForm,
|
||||||
}),
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
setShowEditModal(false);
|
setShowEditModal(false);
|
||||||
await queryClient.invalidateQueries({ queryKey: ["attendance"] });
|
alert.success(result?.message || "Upraveno");
|
||||||
alert.success(result.message);
|
} catch (e) {
|
||||||
} else {
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
alert.error(result.error);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReset = async () => {
|
const handleReset = async () => {
|
||||||
if (!resetConfirm.userId) return;
|
if (!resetConfirm.userId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(
|
await resetMutation.mutateAsync({
|
||||||
`${API_BASE}/attendance?action=balances`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
user_id: resetConfirm.userId,
|
user_id: resetConfirm.userId,
|
||||||
year,
|
year,
|
||||||
action_type: "reset",
|
action_type: "reset",
|
||||||
}),
|
});
|
||||||
},
|
} catch (e) {
|
||||||
);
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
setResetConfirm({ show: false, userId: null, userName: "" });
|
|
||||||
await queryClient.invalidateQueries({ queryKey: ["attendance"] });
|
|
||||||
alert.success(result.message);
|
|
||||||
} else {
|
|
||||||
alert.error(result.error);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -263,11 +255,11 @@ export default function AttendanceBalances() {
|
|||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
transition={{ duration: 0.25, delay: 0.06 }}
|
||||||
>
|
>
|
||||||
<div className="admin-card-body">
|
<div className="admin-card-body">
|
||||||
<Skeleton
|
{balancesPending ? (
|
||||||
name="attendance-balances"
|
<div className="admin-loading">
|
||||||
loading={balancesPending}
|
<div className="admin-spinner" />
|
||||||
fixture={<AttendanceBalancesFixture />}
|
</div>
|
||||||
>
|
) : (
|
||||||
<>
|
<>
|
||||||
{balancesData &&
|
{balancesData &&
|
||||||
Object.keys(balancesData.balances).length === 0 && (
|
Object.keys(balancesData.balances).length === 0 && (
|
||||||
@@ -387,7 +379,7 @@ export default function AttendanceBalances() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
</Skeleton>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@@ -557,13 +549,9 @@ export default function AttendanceBalances() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{fundPending && (
|
{fundPending && (
|
||||||
<Skeleton
|
<div className="admin-loading">
|
||||||
name="attendance-balances-fund"
|
<div className="admin-spinner" />
|
||||||
loading={fundPending}
|
</div>
|
||||||
fixture={<AttendanceBalancesFixture />}
|
|
||||||
>
|
|
||||||
<div className="mt-6" />
|
|
||||||
</Skeleton>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Monthly Project Overview */}
|
{/* Monthly Project Overview */}
|
||||||
@@ -761,44 +749,27 @@ export default function AttendanceBalances() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{projectPending && (
|
{projectPending && (
|
||||||
<Skeleton
|
<div className="admin-loading">
|
||||||
name="attendance-balances-projects"
|
<div className="admin-spinner" />
|
||||||
loading={projectPending}
|
</div>
|
||||||
fixture={<AttendanceBalancesFixture />}
|
|
||||||
>
|
|
||||||
<div className="mt-6" />
|
|
||||||
</Skeleton>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Edit Modal */}
|
{/* Edit Modal */}
|
||||||
<AnimatePresence>
|
<FormModal
|
||||||
{showEditModal && editingUser && (
|
isOpen={showEditModal && !!editingUser}
|
||||||
<motion.div
|
onClose={() => setShowEditModal(false)}
|
||||||
className="admin-modal-overlay"
|
onSubmit={handleEditSubmit}
|
||||||
initial={{ opacity: 0 }}
|
title="Upravit dovolenou"
|
||||||
animate={{ opacity: 1 }}
|
loading={editMutation.isPending}
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
>
|
||||||
<div
|
{editingUser && (
|
||||||
className="admin-modal-backdrop"
|
<>
|
||||||
onClick={() => setShowEditModal(false)}
|
<p
|
||||||
/>
|
className="text-secondary"
|
||||||
<motion.div
|
style={{ marginTop: "0", marginBottom: "0.75rem" }}
|
||||||
className="admin-modal"
|
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
>
|
||||||
<div className="admin-modal-header">
|
|
||||||
<h2 className="admin-modal-title">Upravit dovolenou</h2>
|
|
||||||
<p className="text-secondary" style={{ marginTop: "0.25rem" }}>
|
|
||||||
{editingUser.name}
|
{editingUser.name}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="admin-modal-body">
|
|
||||||
<div className="admin-form">
|
<div className="admin-form">
|
||||||
<FormField label="Nárok na dovolenou (hodiny)">
|
<FormField label="Nárok na dovolenou (hodiny)">
|
||||||
<input
|
<input
|
||||||
@@ -851,28 +822,9 @@ export default function AttendanceBalances() {
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
|
|
||||||
<div className="admin-modal-footer">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowEditModal(false)}
|
|
||||||
className="admin-btn admin-btn-secondary"
|
|
||||||
>
|
|
||||||
Zrušit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleEditSubmit}
|
|
||||||
className="admin-btn admin-btn-primary"
|
|
||||||
>
|
|
||||||
Uložit
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</FormModal>
|
||||||
|
|
||||||
{/* Reset Confirmation */}
|
{/* Reset Confirmation */}
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
@@ -885,6 +837,7 @@ export default function AttendanceBalances() {
|
|||||||
message={`Opravdu chcete vynulovat čerpání dovolené a nemocenské pro ${resetConfirm.userName} za rok ${year}?`}
|
message={`Opravdu chcete vynulovat čerpání dovolené a nemocenské pro ${resetConfirm.userName} za rok ${year}?`}
|
||||||
confirmText="Resetovat"
|
confirmText="Resetovat"
|
||||||
confirmVariant="danger"
|
confirmVariant="danger"
|
||||||
|
loading={resetMutation.isPending}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { userListOptions } from "../lib/queries/users";
|
import { userListOptions } from "../lib/queries/users";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
@@ -9,9 +9,7 @@ import { motion } from "framer-motion";
|
|||||||
|
|
||||||
import AdminDatePicker from "../components/AdminDatePicker";
|
import AdminDatePicker from "../components/AdminDatePicker";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
import apiFetch from "../utils/api";
|
import { useApiMutation } from "../lib/queries/mutations";
|
||||||
import { Skeleton } from "boneyard-js/react";
|
|
||||||
import AttendanceCreateFixture from "../fixtures/AttendanceCreateFixture";
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
interface CreateForm {
|
interface CreateForm {
|
||||||
@@ -34,7 +32,6 @@ export default function AttendanceCreate() {
|
|||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const { data: usersData, isPending: loading } = useQuery(userListOptions());
|
const { data: usersData, isPending: loading } = useQuery(userListOptions());
|
||||||
const users = (usersData ?? []).map((u) => ({
|
const users = (usersData ?? []).map((u) => ({
|
||||||
id: u.id,
|
id: u.id,
|
||||||
@@ -42,6 +39,12 @@ export default function AttendanceCreate() {
|
|||||||
}));
|
}));
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const createMutation = useApiMutation<CreateForm, { message?: string }>({
|
||||||
|
url: () => `${API_BASE}/attendance`,
|
||||||
|
method: () => "POST",
|
||||||
|
invalidate: ["attendance", "users"],
|
||||||
|
});
|
||||||
|
|
||||||
const [form, setForm] = useState<CreateForm>(() => {
|
const [form, setForm] = useState<CreateForm>(() => {
|
||||||
const today = new Date().toISOString().split("T")[0];
|
const today = new Date().toISOString().split("T")[0];
|
||||||
return {
|
return {
|
||||||
@@ -72,23 +75,11 @@ export default function AttendanceCreate() {
|
|||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(`${API_BASE}/attendance`, {
|
const result = await createMutation.mutateAsync(form);
|
||||||
method: "POST",
|
alert.success(result?.message || "Uloženo");
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(form),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
alert.success(result.message);
|
|
||||||
await queryClient.invalidateQueries({ queryKey: ["attendance"] });
|
|
||||||
navigate(`/attendance/admin?month=${form.shift_date.substring(0, 7)}`);
|
navigate(`/attendance/admin?month=${form.shift_date.substring(0, 7)}`);
|
||||||
} else {
|
} catch (e) {
|
||||||
alert.error(result.error);
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
@@ -109,12 +100,15 @@ export default function AttendanceCreate() {
|
|||||||
|
|
||||||
if (!hasPermission("attendance.manage")) return <Forbidden />;
|
if (!hasPermission("attendance.manage")) return <Forbidden />;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="admin-loading">
|
||||||
|
<div className="admin-spinner" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Skeleton
|
|
||||||
name="attendance-create"
|
|
||||||
loading={loading}
|
|
||||||
fixture={<AttendanceCreateFixture />}
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="admin-page-header"
|
className="admin-page-header"
|
||||||
@@ -183,7 +177,6 @@ export default function AttendanceCreate() {
|
|||||||
<option value="work">Práce</option>
|
<option value="work">Práce</option>
|
||||||
<option value="vacation">Dovolená</option>
|
<option value="vacation">Dovolená</option>
|
||||||
<option value="sick">Nemoc</option>
|
<option value="sick">Nemoc</option>
|
||||||
<option value="holiday">Svátek</option>
|
|
||||||
<option value="unpaid">Neplacené volno</option>
|
<option value="unpaid">Neplacené volno</option>
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
@@ -326,6 +319,5 @@ export default function AttendanceCreate() {
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</Skeleton>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,12 @@ import {
|
|||||||
getLeaveTypeBadgeClass,
|
getLeaveTypeBadgeClass,
|
||||||
calculateWorkMinutesPrint,
|
calculateWorkMinutesPrint,
|
||||||
formatTimeOrDatetimePrint,
|
formatTimeOrDatetimePrint,
|
||||||
|
calculateFreeHolidayHours,
|
||||||
|
holidaysInMonth,
|
||||||
|
normalizeDateStr,
|
||||||
|
formatHoursDecimal,
|
||||||
} from "../utils/attendanceHelpers";
|
} from "../utils/attendanceHelpers";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
import { Skeleton } from "boneyard-js/react";
|
|
||||||
import AttendanceHistoryFixture from "../fixtures/AttendanceHistoryFixture";
|
|
||||||
|
|
||||||
const MONTH_NAMES = [
|
const MONTH_NAMES = [
|
||||||
"Leden",
|
"Leden",
|
||||||
@@ -196,7 +198,6 @@ export default function AttendanceHistory() {
|
|||||||
let totalMinutes = 0;
|
let totalMinutes = 0;
|
||||||
let vacationHours = 0;
|
let vacationHours = 0;
|
||||||
let sickHours = 0;
|
let sickHours = 0;
|
||||||
let holidayHours = 0;
|
|
||||||
let unpaidHours = 0;
|
let unpaidHours = 0;
|
||||||
|
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
@@ -208,26 +209,51 @@ export default function AttendanceHistory() {
|
|||||||
record.leave_hours != null ? Number(record.leave_hours) : 8;
|
record.leave_hours != null ? Number(record.leave_hours) : 8;
|
||||||
if (leaveType === "vacation") vacationHours += hours;
|
if (leaveType === "vacation") vacationHours += hours;
|
||||||
else if (leaveType === "sick") sickHours += hours;
|
else if (leaveType === "sick") sickHours += hours;
|
||||||
else if (leaveType === "holiday") holidayHours += hours;
|
|
||||||
else if (leaveType === "unpaid") unpaidHours += hours;
|
else if (leaveType === "unpaid") unpaidHours += hours;
|
||||||
|
// "holiday" is no longer a user-selectable leave_type — it is
|
||||||
|
// auto-computed from Czech public holidays by the print/KPI code.
|
||||||
|
// Old records may still exist; skip so they don't double-count.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const yr = parseInt(yearStr, 10);
|
const yr = parseInt(yearStr, 10);
|
||||||
const mo = parseInt(monthStr, 10) - 1;
|
const mo = parseInt(monthStr, 10) - 1;
|
||||||
const businessDays = getBusinessDaysInMonth(yr, mo);
|
const businessDays = getBusinessDaysInMonth(yr, mo);
|
||||||
const fund = businessDays * 8;
|
// Fund counts Mon-Fri + holidays (holidays are paid via the fond).
|
||||||
|
const holidayDates = holidaysInMonth(yr, mo + 1);
|
||||||
|
const holidayCount = holidayDates.length;
|
||||||
|
const fund = (businessDays + holidayCount) * 8;
|
||||||
const worked = Math.round((totalMinutes / 60) * 100) / 100;
|
const worked = Math.round((totalMinutes / 60) * 100) / 100;
|
||||||
// Covered = worked + vacation + sick (NOT holiday/unpaid — holiday is excluded from fund, unpaid is voluntary)
|
// Covered = worked + vacation + sick (NOT unpaid — unpaid is voluntary).
|
||||||
const leaveHours = vacationHours + sickHours;
|
const leaveHours = vacationHours + sickHours;
|
||||||
const covered = Math.round((worked + leaveHours) * 100) / 100;
|
const covered = Math.round((worked + leaveHours) * 100) / 100;
|
||||||
const remaining = Math.max(0, Math.round((fund - covered) * 100) / 100);
|
|
||||||
const overtime = Math.max(0, Math.round((covered - fund) * 100) / 100);
|
// fondUsed = real_worked + vacation + worked_holiday + free_holiday.
|
||||||
|
// (the all-in number: everything the user got paid for this month).
|
||||||
|
const realWorked = totalMinutes / 60;
|
||||||
|
const freeHolidayHours = calculateFreeHolidayHours(
|
||||||
|
records as unknown as Parameters<typeof calculateFreeHolidayHours>[0],
|
||||||
|
holidayDates,
|
||||||
|
);
|
||||||
|
const workedHolidayHours = records
|
||||||
|
.filter(
|
||||||
|
(r) =>
|
||||||
|
(r.leave_type || "work") === "work" &&
|
||||||
|
holidayDates.includes(normalizeDateStr(r.shift_date)),
|
||||||
|
)
|
||||||
|
.reduce((sum, r) => sum + calculateWorkMinutesPrint(r) / 60, 0);
|
||||||
|
const fondUsed =
|
||||||
|
realWorked + vacationHours + workedHolidayHours + freeHolidayHours;
|
||||||
|
const delta = fondUsed - fund;
|
||||||
|
const missing = Math.max(0, Math.round(-delta * 100) / 100);
|
||||||
|
const overtime = Math.max(0, Math.round(delta * 100) / 100);
|
||||||
|
const remaining = missing;
|
||||||
|
|
||||||
const monthlyFund = {
|
const monthlyFund = {
|
||||||
fund,
|
fund,
|
||||||
business_days: businessDays,
|
business_days: businessDays,
|
||||||
worked,
|
holiday_count: holidayCount,
|
||||||
|
worked: Math.round(fondUsed * 100) / 100,
|
||||||
covered,
|
covered,
|
||||||
remaining,
|
remaining,
|
||||||
overtime,
|
overtime,
|
||||||
@@ -238,9 +264,12 @@ export default function AttendanceHistory() {
|
|||||||
totalMinutes,
|
totalMinutes,
|
||||||
vacationHours,
|
vacationHours,
|
||||||
sickHours,
|
sickHours,
|
||||||
holidayHours,
|
|
||||||
unpaidHours,
|
unpaidHours,
|
||||||
monthlyFund,
|
monthlyFund,
|
||||||
|
fondUsed,
|
||||||
|
freeHolidayHours,
|
||||||
|
workedHolidayHours,
|
||||||
|
delta,
|
||||||
};
|
};
|
||||||
}, [records, month]);
|
}, [records, month]);
|
||||||
|
|
||||||
@@ -324,7 +353,6 @@ export default function AttendanceHistory() {
|
|||||||
}
|
}
|
||||||
.badge-vacation { background: #dbeafe; color: #1d4ed8; }
|
.badge-vacation { background: #dbeafe; color: #1d4ed8; }
|
||||||
.badge-sick { background: #fee2e2; color: #dc2626; }
|
.badge-sick { background: #fee2e2; color: #dc2626; }
|
||||||
.badge-holiday { background: #dcfce7; color: #16a34a; }
|
|
||||||
.badge-unpaid { background: #f3f4f6; color: #6b7280; }
|
.badge-unpaid { background: #f3f4f6; color: #6b7280; }
|
||||||
.badge-overtime { background: #fef3c7; color: #d97706; }
|
.badge-overtime { background: #fef3c7; color: #d97706; }
|
||||||
@media print {
|
@media print {
|
||||||
@@ -411,11 +439,11 @@ export default function AttendanceHistory() {
|
|||||||
transition={{ duration: 0.25, delay: 0.08 }}
|
transition={{ duration: 0.25, delay: 0.08 }}
|
||||||
>
|
>
|
||||||
<div className="admin-card-body">
|
<div className="admin-card-body">
|
||||||
<Skeleton
|
{isPending ? (
|
||||||
name="attendance-history-fund"
|
<div className="admin-loading">
|
||||||
loading={isPending}
|
<div className="admin-spinner" />
|
||||||
fixture={<AttendanceHistoryFixture />}
|
</div>
|
||||||
>
|
) : (
|
||||||
<>
|
<>
|
||||||
{computed.monthlyFund && (
|
{computed.monthlyFund && (
|
||||||
<div
|
<div
|
||||||
@@ -465,51 +493,90 @@ export default function AttendanceHistory() {
|
|||||||
style={{ fontSize: "0.8125rem" }}
|
style={{ fontSize: "0.8125rem" }}
|
||||||
>
|
>
|
||||||
{computed.monthlyFund.business_days} prac. dnů
|
{computed.monthlyFund.business_days} prac. dnů
|
||||||
|
{computed.monthlyFund.holiday_count > 0 &&
|
||||||
|
` + ${computed.monthlyFund.holiday_count} svátky`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="attendance-balance-bar">
|
<div className="attendance-balance-bar">
|
||||||
<div
|
<div
|
||||||
className="attendance-balance-progress"
|
className="attendance-balance-progress"
|
||||||
style={{
|
style={{
|
||||||
width: `${Math.min(100, computed.monthlyFund.fund > 0 ? (computed.monthlyFund.covered / computed.monthlyFund.fund) * 100 : 0)}%`,
|
width: `${Math.min(100, computed.monthlyFund.fund > 0 ? (computed.monthlyFund.worked / computed.monthlyFund.fund) * 100 : 0)}%`,
|
||||||
background:
|
background:
|
||||||
computed.monthlyFund.covered >=
|
computed.delta > 0
|
||||||
computed.monthlyFund.fund
|
? "linear-gradient(135deg, var(--warning), #d97706)"
|
||||||
|
: computed.delta >= 0
|
||||||
? "linear-gradient(135deg, var(--success), #059669)"
|
? "linear-gradient(135deg, var(--success), #059669)"
|
||||||
: "var(--gradient)",
|
: "var(--gradient)",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="text-muted"
|
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: "0.4rem",
|
||||||
|
marginTop: "0.5rem",
|
||||||
|
minHeight: "1.5rem",
|
||||||
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
fontSize: "0.75rem",
|
|
||||||
marginTop: "0.375rem",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>
|
<div
|
||||||
{"Pokryto: "}
|
style={{
|
||||||
{computed.monthlyFund.covered}h (práce{" "}
|
display: "flex",
|
||||||
{computed.monthlyFund.worked}h
|
flexWrap: "wrap",
|
||||||
{computed.vacationHours > 0 &&
|
gap: "0.4rem",
|
||||||
` + dovolená ${computed.vacationHours}h`}
|
}}
|
||||||
{computed.sickHours > 0 &&
|
>
|
||||||
` + nemoc ${computed.sickHours}h`}
|
{computed.totalMinutes > 0 && (
|
||||||
{computed.holidayHours > 0 &&
|
<span
|
||||||
` + svátek ${computed.holidayHours}h`}
|
className="attendance-leave-badge"
|
||||||
{computed.unpaidHours > 0 &&
|
title="Odpracováno (reálně)"
|
||||||
` + neplacené ${computed.unpaidHours}h`}
|
>
|
||||||
)
|
Práce: {formatHoursDecimal(computed.totalMinutes)}h
|
||||||
</span>
|
</span>
|
||||||
{computed.monthlyFund.overtime > 0 ? (
|
|
||||||
<span className="text-warning fw-600">
|
|
||||||
Přesčas: +{computed.monthlyFund.overtime}h
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span>Zbývá: {computed.monthlyFund.remaining}h</span>
|
|
||||||
)}
|
)}
|
||||||
|
{computed.vacationHours > 0 && (
|
||||||
|
<span className="attendance-leave-badge badge-vacation">
|
||||||
|
Dov: {computed.vacationHours}h
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{computed.sickHours > 0 && (
|
||||||
|
<span className="attendance-leave-badge badge-sick">
|
||||||
|
Nem: {computed.sickHours}h
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{computed.freeHolidayHours > 0 && (
|
||||||
|
<span className="attendance-leave-badge badge-holiday">
|
||||||
|
Sv: {Math.round(computed.freeHolidayHours)}h
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{computed.unpaidHours > 0 && (
|
||||||
|
<span className="attendance-leave-badge badge-unpaid">
|
||||||
|
Nep: {computed.unpaidHours}h
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
color: "var(--text-muted)",
|
||||||
|
}}
|
||||||
|
className={
|
||||||
|
computed.delta > 0
|
||||||
|
? "text-warning fw-600"
|
||||||
|
: computed.delta < 0
|
||||||
|
? "text-danger fw-600"
|
||||||
|
: "text-success fw-600"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{computed.delta > 0
|
||||||
|
? `Přesčas: +${formatHoursDecimal(computed.delta * 60)}h`
|
||||||
|
: computed.delta < 0
|
||||||
|
? `Zbývá: ${formatHoursDecimal(-computed.delta * 60)}h`
|
||||||
|
: "Fond splněn"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -527,7 +594,7 @@ export default function AttendanceHistory() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
</Skeleton>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@@ -539,11 +606,11 @@ export default function AttendanceHistory() {
|
|||||||
transition={{ duration: 0.25, delay: 0.12 }}
|
transition={{ duration: 0.25, delay: 0.12 }}
|
||||||
>
|
>
|
||||||
<div className="admin-card-body">
|
<div className="admin-card-body">
|
||||||
<Skeleton
|
{isPending ? (
|
||||||
name="attendance-history-table"
|
<div className="admin-loading">
|
||||||
loading={isPending}
|
<div className="admin-spinner" />
|
||||||
fixture={<AttendanceHistoryFixture />}
|
</div>
|
||||||
>
|
) : (
|
||||||
<>
|
<>
|
||||||
{records.length === 0 && (
|
{records.length === 0 && (
|
||||||
<div className="admin-empty-state">
|
<div className="admin-empty-state">
|
||||||
@@ -622,7 +689,7 @@ export default function AttendanceHistory() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
</Skeleton>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@@ -670,9 +737,7 @@ export default function AttendanceHistory() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(computed.vacationHours > 0 ||
|
{(computed.vacationHours > 0 || computed.sickHours > 0) && (
|
||||||
computed.sickHours > 0 ||
|
|
||||||
computed.holidayHours > 0) && (
|
|
||||||
<div className="leave-summary">
|
<div className="leave-summary">
|
||||||
{computed.vacationHours > 0 && (
|
{computed.vacationHours > 0 && (
|
||||||
<>
|
<>
|
||||||
@@ -688,13 +753,6 @@ export default function AttendanceHistory() {
|
|||||||
</span>{" "}
|
</span>{" "}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{computed.holidayHours > 0 && (
|
|
||||||
<>
|
|
||||||
<span className="leave-badge badge-holiday">
|
|
||||||
Svátek: {computed.holidayHours}h
|
|
||||||
</span>{" "}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import {
|
|||||||
attendanceLocationOptions,
|
attendanceLocationOptions,
|
||||||
type LocationRecord,
|
type LocationRecord,
|
||||||
} from "../lib/queries/attendance";
|
} from "../lib/queries/attendance";
|
||||||
import { Skeleton } from "boneyard-js/react";
|
|
||||||
import AttendanceLocationFixture from "../fixtures/AttendanceLocationFixture";
|
|
||||||
|
|
||||||
export default function AttendanceLocation() {
|
export default function AttendanceLocation() {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
@@ -168,12 +166,15 @@ export default function AttendanceLocation() {
|
|||||||
: record.shift_date;
|
: record.shift_date;
|
||||||
const month = shiftDateStr.substring(0, 7);
|
const month = shiftDateStr.substring(0, 7);
|
||||||
|
|
||||||
|
if (isPending) {
|
||||||
|
return (
|
||||||
|
<div className="admin-loading">
|
||||||
|
<div className="admin-spinner" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Skeleton
|
|
||||||
name="attendance-location"
|
|
||||||
loading={isPending}
|
|
||||||
fixture={<AttendanceLocationFixture />}
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="admin-page-header"
|
className="admin-page-header"
|
||||||
@@ -288,6 +289,5 @@ export default function AttendanceLocation() {
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</Skeleton>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import Pagination from "../components/Pagination";
|
import Pagination from "../components/Pagination";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
|
import FormModal from "../components/FormModal";
|
||||||
import AdminDatePicker from "../components/AdminDatePicker";
|
import AdminDatePicker from "../components/AdminDatePicker";
|
||||||
import { czechPlural } from "../utils/formatters";
|
import { czechPlural } from "../utils/formatters";
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
import { Skeleton } from "boneyard-js/react";
|
import { useApiMutation } from "../lib/queries/mutations";
|
||||||
import AuditLogFixture from "../fixtures/AuditLogFixture";
|
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
@@ -91,7 +91,6 @@ interface Filters {
|
|||||||
export default function AuditLog() {
|
export default function AuditLog() {
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const [filters, setFilters] = useState<Filters>({
|
const [filters, setFilters] = useState<Filters>({
|
||||||
search: "",
|
search: "",
|
||||||
action: "",
|
action: "",
|
||||||
@@ -103,7 +102,6 @@ export default function AuditLog() {
|
|||||||
const [perPage, setPerPage] = useState(50);
|
const [perPage, setPerPage] = useState(50);
|
||||||
const [showCleanup, setShowCleanup] = useState(false);
|
const [showCleanup, setShowCleanup] = useState(false);
|
||||||
const [cleanupDays, setCleanupDays] = useState(90);
|
const [cleanupDays, setCleanupDays] = useState(90);
|
||||||
const [cleaning, setCleaning] = useState(false);
|
|
||||||
|
|
||||||
const { data: logsData, isPending } = useQuery({
|
const { data: logsData, isPending } = useQuery({
|
||||||
queryKey: [
|
queryKey: [
|
||||||
@@ -148,13 +146,26 @@ export default function AuditLog() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const logs = logsData?.data ?? [];
|
const logs: AuditLogEntry[] = logsData?.data ?? [];
|
||||||
const pagination = logsData?.pagination ?? null;
|
const pagination = logsData?.pagination ?? null;
|
||||||
|
|
||||||
if (!hasPermission("settings.audit")) {
|
if (!hasPermission("settings.audit")) {
|
||||||
return <Forbidden />;
|
return <Forbidden />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cleanupMutation = useApiMutation<
|
||||||
|
{ days: number; confirm?: string },
|
||||||
|
{ message?: string; error?: string }
|
||||||
|
>({
|
||||||
|
url: () => `${API_BASE}/audit-log/cleanup`,
|
||||||
|
method: () => "POST",
|
||||||
|
invalidate: ["audit-log"],
|
||||||
|
onSuccess: (data) => {
|
||||||
|
alert.success(data?.message || "Hotovo");
|
||||||
|
setShowCleanup(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const handleFilterChange = (key: keyof Filters, value: string) => {
|
const handleFilterChange = (key: keyof Filters, value: string) => {
|
||||||
setFilters((prev) => ({ ...prev, [key]: value }));
|
setFilters((prev) => ({ ...prev, [key]: value }));
|
||||||
setPage(1);
|
setPage(1);
|
||||||
@@ -170,25 +181,14 @@ export default function AuditLog() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCleanup = async () => {
|
const handleCleanup = async () => {
|
||||||
setCleaning(true);
|
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(`${API_BASE}/audit-log/cleanup`, {
|
// Backend requires this literal confirmation token when wiping everything.
|
||||||
method: "POST",
|
await cleanupMutation.mutateAsync({
|
||||||
headers: { "Content-Type": "application/json" },
|
days: cleanupDays,
|
||||||
body: JSON.stringify({ days: cleanupDays }),
|
...(cleanupDays === 0 ? { confirm: "DELETE_ALL_AUDIT" } : {}),
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
} catch (e) {
|
||||||
if (data.success) {
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
alert.success(data.message);
|
|
||||||
setShowCleanup(false);
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["audit-log"] });
|
|
||||||
} else {
|
|
||||||
alert.error(data.error);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
|
||||||
setCleaning(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -199,13 +199,9 @@ export default function AuditLog() {
|
|||||||
|
|
||||||
if (isPending && logs.length === 0) {
|
if (isPending && logs.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Skeleton
|
<div className="admin-loading">
|
||||||
name="audit-log"
|
<div className="admin-spinner" />
|
||||||
loading={isPending && logs.length === 0}
|
</div>
|
||||||
fixture={<AuditLogFixture />}
|
|
||||||
>
|
|
||||||
<div />
|
|
||||||
</Skeleton>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,37 +241,16 @@ export default function AuditLog() {
|
|||||||
</button>
|
</button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{showCleanup && (
|
<FormModal
|
||||||
<div className="admin-modal-overlay" style={{ opacity: 1 }}>
|
isOpen={showCleanup}
|
||||||
<div
|
onClose={() => !cleanupMutation.isPending && setShowCleanup(false)}
|
||||||
className="admin-modal-backdrop"
|
onSubmit={handleCleanup}
|
||||||
onClick={() => !cleaning && setShowCleanup(false)}
|
title="Vyčistit audit log"
|
||||||
/>
|
submitLabel="Smazat"
|
||||||
<motion.div
|
loading={cleanupMutation.isPending}
|
||||||
className="admin-modal admin-confirm-modal"
|
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
>
|
||||||
<div className="admin-modal-body admin-confirm-content">
|
<p className="admin-confirm-message">Smazat záznamy starší než:</p>
|
||||||
<div className="admin-confirm-icon admin-confirm-icon-danger">
|
<div style={{ margin: "0.75rem 0", maxWidth: "200px" }}>
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<polyline points="3 6 5 6 21 6" />
|
|
||||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h2 className="admin-confirm-title">Vyčistit audit log</h2>
|
|
||||||
<p className="admin-confirm-message">
|
|
||||||
Smazat záznamy starší než:
|
|
||||||
</p>
|
|
||||||
<div style={{ margin: "0.75rem auto", maxWidth: "200px" }}>
|
|
||||||
<select
|
<select
|
||||||
className="admin-form-select"
|
className="admin-form-select"
|
||||||
value={cleanupDays}
|
value={cleanupDays}
|
||||||
@@ -295,28 +270,7 @@ export default function AuditLog() {
|
|||||||
>
|
>
|
||||||
Tato akce je nevratná.
|
Tato akce je nevratná.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</FormModal>
|
||||||
<div className="admin-modal-footer">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowCleanup(false)}
|
|
||||||
className="admin-btn admin-btn-secondary"
|
|
||||||
disabled={cleaning}
|
|
||||||
>
|
|
||||||
Zrušit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleCleanup}
|
|
||||||
className="admin-btn admin-btn-primary"
|
|
||||||
disabled={cleaning}
|
|
||||||
>
|
|
||||||
{cleaning ? "Mažu..." : "Smazat"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
className="admin-card mb-4"
|
className="admin-card mb-4"
|
||||||
@@ -391,90 +345,11 @@ export default function AuditLog() {
|
|||||||
>
|
>
|
||||||
<div className="admin-card-body">
|
<div className="admin-card-body">
|
||||||
<div className="admin-table-responsive">
|
<div className="admin-table-responsive">
|
||||||
<Skeleton
|
{isPending ? (
|
||||||
name="audit-log-rows"
|
<div className="admin-loading">
|
||||||
loading={isPending}
|
<div className="admin-spinner" />
|
||||||
fixture={
|
</div>
|
||||||
<table className="admin-table">
|
) : (
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Čas</th>
|
|
||||||
<th>Uživatel</th>
|
|
||||||
<th>Akce</th>
|
|
||||||
<th>Typ entity</th>
|
|
||||||
<th>Popis</th>
|
|
||||||
<th>IP</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Array.from({ length: 10 }, (_, i) => (
|
|
||||||
<tr key={i}>
|
|
||||||
<td>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: 110,
|
|
||||||
height: 14,
|
|
||||||
background: "var(--bg-tertiary)",
|
|
||||||
borderRadius: 4,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: 80,
|
|
||||||
height: 14,
|
|
||||||
background: "var(--bg-tertiary)",
|
|
||||||
borderRadius: 4,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: 70,
|
|
||||||
height: 22,
|
|
||||||
background: "var(--bg-tertiary)",
|
|
||||||
borderRadius: 10,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: 80,
|
|
||||||
height: 14,
|
|
||||||
background: "var(--bg-tertiary)",
|
|
||||||
borderRadius: 4,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
height: 14,
|
|
||||||
background: "var(--bg-tertiary)",
|
|
||||||
borderRadius: 4,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: 90,
|
|
||||||
height: 14,
|
|
||||||
background: "var(--bg-tertiary)",
|
|
||||||
borderRadius: 4,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<table className="admin-table">
|
<table className="admin-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -536,7 +411,7 @@ export default function AuditLog() {
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</Skeleton>
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Pagination
|
<Pagination
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ import { bankAccountsOptions, type BankAccount } from "../lib/queries/common";
|
|||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
import { Skeleton } from "boneyard-js/react";
|
import { useApiMutation } from "../lib/queries/mutations";
|
||||||
import CompanySettingsFixture from "../fixtures/CompanySettingsFixture";
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
const DEFAULT_FIELD_ORDER = [
|
const DEFAULT_FIELD_ORDER = [
|
||||||
@@ -54,6 +53,12 @@ interface CompanyForm {
|
|||||||
quotation_prefix: string;
|
quotation_prefix: string;
|
||||||
order_type_code: string;
|
order_type_code: string;
|
||||||
invoice_type_code: string;
|
invoice_type_code: string;
|
||||||
|
warehouse_receipt_prefix: string;
|
||||||
|
warehouse_receipt_number_pattern: string;
|
||||||
|
warehouse_issue_prefix: string;
|
||||||
|
warehouse_issue_number_pattern: string;
|
||||||
|
warehouse_inventory_prefix: string;
|
||||||
|
warehouse_inventory_number_pattern: string;
|
||||||
default_currency: string;
|
default_currency: string;
|
||||||
default_vat_rate: number;
|
default_vat_rate: number;
|
||||||
available_currencies: string[];
|
available_currencies: string[];
|
||||||
@@ -99,6 +104,12 @@ export default function CompanySettings({
|
|||||||
quotation_prefix: "",
|
quotation_prefix: "",
|
||||||
order_type_code: "",
|
order_type_code: "",
|
||||||
invoice_type_code: "",
|
invoice_type_code: "",
|
||||||
|
warehouse_receipt_prefix: "",
|
||||||
|
warehouse_receipt_number_pattern: "",
|
||||||
|
warehouse_issue_prefix: "",
|
||||||
|
warehouse_issue_number_pattern: "",
|
||||||
|
warehouse_inventory_prefix: "",
|
||||||
|
warehouse_inventory_number_pattern: "",
|
||||||
default_currency: "CZK",
|
default_currency: "CZK",
|
||||||
default_vat_rate: 21,
|
default_vat_rate: 21,
|
||||||
available_currencies: ["CZK", "EUR", "USD", "GBP"],
|
available_currencies: ["CZK", "EUR", "USD", "GBP"],
|
||||||
@@ -124,6 +135,40 @@ export default function CompanySettings({
|
|||||||
is_default: false,
|
is_default: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const saveSettingsMutation = useApiMutation<
|
||||||
|
Record<string, unknown>,
|
||||||
|
{ message?: string }
|
||||||
|
>({
|
||||||
|
url: () => `${API_BASE}/company-settings`,
|
||||||
|
method: () => "PUT",
|
||||||
|
invalidate: [
|
||||||
|
"settings",
|
||||||
|
"company-settings",
|
||||||
|
"attendance",
|
||||||
|
"offers",
|
||||||
|
"orders",
|
||||||
|
"invoices",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const bankMutation = useApiMutation<
|
||||||
|
{ id?: number; bank: BankForm },
|
||||||
|
{ message?: string }
|
||||||
|
>({
|
||||||
|
url: (input) =>
|
||||||
|
input.id != null
|
||||||
|
? `${API_BASE}/bank-accounts/${input.id}`
|
||||||
|
: `${API_BASE}/bank-accounts`,
|
||||||
|
method: (input) => (input.id != null ? "PUT" : "POST"),
|
||||||
|
invalidate: ["settings", "bank-accounts", "invoices"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const bankDeleteMutation = useApiMutation<number, { message?: string }>({
|
||||||
|
url: (id) => `${API_BASE}/bank-accounts/${id}`,
|
||||||
|
method: () => "DELETE",
|
||||||
|
invalidate: ["settings", "bank-accounts", "invoices"],
|
||||||
|
});
|
||||||
|
|
||||||
const getFullFieldOrder = useCallback((): string[] => {
|
const getFullFieldOrder = useCallback((): string[] => {
|
||||||
const allBuiltIn = [...DEFAULT_FIELD_ORDER];
|
const allBuiltIn = [...DEFAULT_FIELD_ORDER];
|
||||||
const order = [...fieldOrder].filter((k) => k !== "company_name");
|
const order = [...fieldOrder].filter((k) => k !== "company_name");
|
||||||
@@ -224,6 +269,15 @@ export default function CompanySettings({
|
|||||||
quotation_prefix: settingsData.quotation_prefix || "",
|
quotation_prefix: settingsData.quotation_prefix || "",
|
||||||
order_type_code: settingsData.order_type_code || "",
|
order_type_code: settingsData.order_type_code || "",
|
||||||
invoice_type_code: settingsData.invoice_type_code || "",
|
invoice_type_code: settingsData.invoice_type_code || "",
|
||||||
|
warehouse_receipt_prefix: settingsData.warehouse_receipt_prefix || "",
|
||||||
|
warehouse_receipt_number_pattern:
|
||||||
|
settingsData.warehouse_receipt_number_pattern || "",
|
||||||
|
warehouse_issue_prefix: settingsData.warehouse_issue_prefix || "",
|
||||||
|
warehouse_issue_number_pattern:
|
||||||
|
settingsData.warehouse_issue_number_pattern || "",
|
||||||
|
warehouse_inventory_prefix: settingsData.warehouse_inventory_prefix || "",
|
||||||
|
warehouse_inventory_number_pattern:
|
||||||
|
settingsData.warehouse_inventory_number_pattern || "",
|
||||||
default_currency: settingsData.default_currency || "CZK",
|
default_currency: settingsData.default_currency || "CZK",
|
||||||
default_vat_rate: settingsData.default_vat_rate ?? 21,
|
default_vat_rate: settingsData.default_vat_rate ?? 21,
|
||||||
available_currencies:
|
available_currencies:
|
||||||
@@ -283,26 +337,14 @@ export default function CompanySettings({
|
|||||||
}
|
}
|
||||||
setBankSaving(true);
|
setBankSaving(true);
|
||||||
try {
|
try {
|
||||||
const isEdit = editingBank !== null;
|
const result = await bankMutation.mutateAsync({
|
||||||
const url = isEdit
|
id: editingBank ?? undefined,
|
||||||
? `${API_BASE}/bank-accounts/${editingBank}`
|
bank: bankForm,
|
||||||
: `${API_BASE}/bank-accounts`;
|
|
||||||
const response = await apiFetch(url, {
|
|
||||||
method: isEdit ? "PUT" : "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(bankForm),
|
|
||||||
});
|
});
|
||||||
const result = await response.json();
|
alert.success(result?.message || "Uloženo");
|
||||||
if (result.success) {
|
|
||||||
alert.success(result.message);
|
|
||||||
resetBankForm();
|
resetBankForm();
|
||||||
queryClient.invalidateQueries({ queryKey: ["bank-accounts"] });
|
} catch (e) {
|
||||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
} else {
|
|
||||||
alert.error(result.error || "Chyba při ukládání");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
} finally {
|
||||||
setBankSaving(false);
|
setBankSaving(false);
|
||||||
}
|
}
|
||||||
@@ -315,23 +357,11 @@ export default function CompanySettings({
|
|||||||
const confirmBankDelete = async () => {
|
const confirmBankDelete = async () => {
|
||||||
if (bankDeleteConfirm.id == null) return;
|
if (bankDeleteConfirm.id == null) return;
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(
|
const result = await bankDeleteMutation.mutateAsync(bankDeleteConfirm.id);
|
||||||
`${API_BASE}/bank-accounts/${bankDeleteConfirm.id}`,
|
alert.success(result?.message || "Smazáno");
|
||||||
{
|
|
||||||
method: "DELETE",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
alert.success(result.message);
|
|
||||||
if (editingBank === bankDeleteConfirm.id) resetBankForm();
|
if (editingBank === bankDeleteConfirm.id) resetBankForm();
|
||||||
queryClient.invalidateQueries({ queryKey: ["bank-accounts"] });
|
} catch (e) {
|
||||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
} else {
|
|
||||||
alert.error(result.error || "Chyba při mazání");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
} finally {
|
||||||
setBankDeleteConfirm({ isOpen: false, id: null });
|
setBankDeleteConfirm({ isOpen: false, id: null });
|
||||||
}
|
}
|
||||||
@@ -368,23 +398,12 @@ export default function CompanySettings({
|
|||||||
),
|
),
|
||||||
supplier_field_order: getFullFieldOrder(),
|
supplier_field_order: getFullFieldOrder(),
|
||||||
};
|
};
|
||||||
const response = await apiFetch(`${API_BASE}/company-settings`, {
|
const result = await saveSettingsMutation.mutateAsync(payload);
|
||||||
method: "PUT",
|
alert.success(result?.message || "Nastavení bylo uloženo");
|
||||||
headers: { "Content-Type": "application/json" },
|
} catch (e) {
|
||||||
body: JSON.stringify(payload),
|
alert.error(
|
||||||
});
|
e instanceof Error ? e.message : "Nepodařilo se uložit nastavení",
|
||||||
const result = await response.json();
|
);
|
||||||
if (result.success) {
|
|
||||||
alert.success(result.message || "Nastavení bylo uloženo");
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["company-settings"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
|
||||||
} else {
|
|
||||||
alert.error(result.error || "Nepodařilo se uložit nastavení");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -438,13 +457,9 @@ export default function CompanySettings({
|
|||||||
|
|
||||||
if (settingsLoading) {
|
if (settingsLoading) {
|
||||||
return (
|
return (
|
||||||
<Skeleton
|
<div className="admin-loading">
|
||||||
name="company-settings"
|
<div className="admin-spinner" />
|
||||||
loading={settingsLoading}
|
</div>
|
||||||
fixture={<CompanySettingsFixture />}
|
|
||||||
>
|
|
||||||
<div />
|
|
||||||
</Skeleton>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -755,13 +770,9 @@ export default function CompanySettings({
|
|||||||
</div>
|
</div>
|
||||||
<div className="admin-card-body">
|
<div className="admin-card-body">
|
||||||
{bankLoading ? (
|
{bankLoading ? (
|
||||||
<Skeleton
|
<div className="admin-loading">
|
||||||
name="company-settings-bank"
|
<div className="admin-spinner" />
|
||||||
loading={bankLoading}
|
</div>
|
||||||
fixture={<CompanySettingsFixture />}
|
|
||||||
>
|
|
||||||
<div />
|
|
||||||
</Skeleton>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{bankAccountsList.length > 0 && (
|
{bankAccountsList.length > 0 && (
|
||||||
@@ -1106,7 +1117,7 @@ export default function CompanySettings({
|
|||||||
>
|
>
|
||||||
<strong>Dostupné zástupné znaky:</strong>{" "}
|
<strong>Dostupné zástupné znaky:</strong>{" "}
|
||||||
<code>{"{YYYY}"}</code> rok, <code>{"{YY}"}</code> rok (2
|
<code>{"{YYYY}"}</code> rok, <code>{"{YY}"}</code> rok (2
|
||||||
číslice), <code>{"{PREFIX}"}</code> prefix nabídky,{" "}
|
číslice), <code>{"{PREFIX}"}</code> prefix (nabídky / sklad),{" "}
|
||||||
<code>{"{CODE}"}</code> typový kód, <code>{"{NNN}"}</code>{" "}
|
<code>{"{CODE}"}</code> typový kód, <code>{"{NNN}"}</code>{" "}
|
||||||
pořadí (3 číslice), <code>{"{NNNN}"}</code> pořadí (4
|
pořadí (3 číslice), <code>{"{NNNN}"}</code> pořadí (4
|
||||||
číslice), <code>{"{NNNNN}"}</code> pořadí (5 číslic)
|
číslice), <code>{"{NNNNN}"}</code> pořadí (5 číslic)
|
||||||
@@ -1136,6 +1147,30 @@ export default function CompanySettings({
|
|||||||
codeKey: "invoice_type_code" as const,
|
codeKey: "invoice_type_code" as const,
|
||||||
codeLabel: "Typový kód",
|
codeLabel: "Typový kód",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Sklad — Příjmy",
|
||||||
|
patternKey: "warehouse_receipt_number_pattern" as const,
|
||||||
|
defaultPattern: "{PREFIX}-{YYYY}-{NNN}",
|
||||||
|
prefixKey: "warehouse_receipt_prefix" as const,
|
||||||
|
prefixLabel: "Prefix",
|
||||||
|
codeKey: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Sklad — Výdeje",
|
||||||
|
patternKey: "warehouse_issue_number_pattern" as const,
|
||||||
|
defaultPattern: "{PREFIX}-{YYYY}-{NNN}",
|
||||||
|
prefixKey: "warehouse_issue_prefix" as const,
|
||||||
|
prefixLabel: "Prefix",
|
||||||
|
codeKey: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Sklad — Inventarizace",
|
||||||
|
patternKey: "warehouse_inventory_number_pattern" as const,
|
||||||
|
defaultPattern: "{PREFIX}-{YYYY}-{NNN}",
|
||||||
|
prefixKey: "warehouse_inventory_prefix" as const,
|
||||||
|
prefixLabel: "Prefix",
|
||||||
|
codeKey: null,
|
||||||
|
},
|
||||||
].map((cfg, idx) => {
|
].map((cfg, idx) => {
|
||||||
const pattern = form[cfg.patternKey] || cfg.defaultPattern;
|
const pattern = form[cfg.patternKey] || cfg.defaultPattern;
|
||||||
const yyyy = String(new Date().getFullYear());
|
const yyyy = String(new Date().getFullYear());
|
||||||
|
|||||||
@@ -9,14 +9,13 @@ import apiFetch from "../utils/api";
|
|||||||
import { dashboardOptions } from "../lib/queries/dashboard";
|
import { dashboardOptions } from "../lib/queries/dashboard";
|
||||||
import { require2FAOptions } from "../lib/queries/settings";
|
import { require2FAOptions } from "../lib/queries/settings";
|
||||||
import { getCzechDate } from "../utils/dashboardHelpers";
|
import { getCzechDate } from "../utils/dashboardHelpers";
|
||||||
|
import { useApiMutation } from "../lib/queries/mutations";
|
||||||
import DashKpiCards from "../components/dashboard/DashKpiCards";
|
import DashKpiCards from "../components/dashboard/DashKpiCards";
|
||||||
import DashQuickActions from "../components/dashboard/DashQuickActions";
|
import DashQuickActions from "../components/dashboard/DashQuickActions";
|
||||||
import DashActivityFeed from "../components/dashboard/DashActivityFeed";
|
import DashActivityFeed from "../components/dashboard/DashActivityFeed";
|
||||||
import DashAttendanceToday from "../components/dashboard/DashAttendanceToday";
|
import DashAttendanceToday from "../components/dashboard/DashAttendanceToday";
|
||||||
import DashProfile from "../components/dashboard/DashProfile";
|
import DashProfile from "../components/dashboard/DashProfile";
|
||||||
import DashSessions from "../components/dashboard/DashSessions";
|
import DashSessions from "../components/dashboard/DashSessions";
|
||||||
import { Skeleton } from "boneyard-js/react";
|
|
||||||
import DashboardFixture from "../fixtures/DashboardFixture";
|
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
@@ -84,6 +83,15 @@ export default function Dashboard() {
|
|||||||
useQuery(require2FAOptions());
|
useQuery(require2FAOptions());
|
||||||
const totpEnabled = totpData?.require_2fa ?? !!user?.totpEnabled;
|
const totpEnabled = totpData?.require_2fa ?? !!user?.totpEnabled;
|
||||||
|
|
||||||
|
const punchMutation = useApiMutation<
|
||||||
|
Record<string, unknown>,
|
||||||
|
{ message?: string }
|
||||||
|
>({
|
||||||
|
url: () => `${API_BASE}/attendance`,
|
||||||
|
method: () => "POST",
|
||||||
|
invalidate: ["attendance", "dashboard"],
|
||||||
|
});
|
||||||
|
|
||||||
// 2FA state - sdileny mezi profilem a bannerem
|
// 2FA state - sdileny mezi profilem a bannerem
|
||||||
const [show2FASetup, setShow2FASetup] = useState(false);
|
const [show2FASetup, setShow2FASetup] = useState(false);
|
||||||
const [show2FADisable, setShow2FADisable] = useState(false);
|
const [show2FADisable, setShow2FADisable] = useState(false);
|
||||||
@@ -104,21 +112,13 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
const submitPunch = async (gpsData: Record<string, unknown> = {}) => {
|
const submitPunch = async (gpsData: Record<string, unknown> = {}) => {
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(`${API_BASE}/attendance`, {
|
const result = await punchMutation.mutateAsync({
|
||||||
method: "POST",
|
punch_action: action,
|
||||||
headers: { "Content-Type": "application/json" },
|
...gpsData,
|
||||||
body: JSON.stringify({ punch_action: action, ...gpsData }),
|
|
||||||
});
|
});
|
||||||
const result = await response.json();
|
alert.success(result?.message || "Docházka zaznamenána");
|
||||||
if (result.success) {
|
} catch (e) {
|
||||||
alert.success(result.data?.message || "Docházka zaznamenána");
|
alert.error(e instanceof Error ? e.message : "Chyba pripojeni");
|
||||||
queryClient.invalidateQueries({ queryKey: ["dashboard"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["attendance"] });
|
|
||||||
} else {
|
|
||||||
alert.error(result.error || "Chyba při záznamu docházky");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba pripojeni");
|
|
||||||
} finally {
|
} finally {
|
||||||
setPunching(false);
|
setPunching(false);
|
||||||
}
|
}
|
||||||
@@ -137,7 +137,7 @@ export default function Dashboard() {
|
|||||||
() => submitPunch({}),
|
() => submitPunch({}),
|
||||||
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 },
|
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 },
|
||||||
);
|
);
|
||||||
}, [dashData, alert, queryClient]);
|
}, [dashData, alert, punchMutation]);
|
||||||
|
|
||||||
// 2FA handlery
|
// 2FA handlery
|
||||||
const handleStart2FASetup = async () => {
|
const handleStart2FASetup = async () => {
|
||||||
@@ -309,15 +309,11 @@ export default function Dashboard() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Skeleton loading */}
|
{/* Loading spinner */}
|
||||||
{dashLoading && (
|
{dashLoading && (
|
||||||
<Skeleton
|
<div className="admin-loading">
|
||||||
name="dashboard"
|
<div className="admin-spinner" />
|
||||||
loading={dashLoading}
|
</div>
|
||||||
fixture={<DashboardFixture />}
|
|
||||||
>
|
|
||||||
<div />
|
|
||||||
</Skeleton>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* KPI cards — only show if user has any admin-level permissions */}
|
{/* KPI cards — only show if user has any admin-level permissions */}
|
||||||
@@ -325,12 +321,14 @@ export default function Dashboard() {
|
|||||||
(hasPermission("offers.view") ||
|
(hasPermission("offers.view") ||
|
||||||
hasPermission("invoices.view") ||
|
hasPermission("invoices.view") ||
|
||||||
hasPermission("projects.view") ||
|
hasPermission("projects.view") ||
|
||||||
hasPermission("orders.view")) && <DashKpiCards dashData={dashData} />}
|
hasPermission("orders.view")) && (
|
||||||
|
<DashKpiCards dashData={dashData ?? null} />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Quick actions */}
|
{/* Quick actions */}
|
||||||
{!dashLoading && (
|
{!dashLoading && (
|
||||||
<DashQuickActions
|
<DashQuickActions
|
||||||
dashData={dashData}
|
dashData={dashData ?? null}
|
||||||
punching={punching}
|
punching={punching}
|
||||||
onPunch={handleQuickPunch}
|
onPunch={handleQuickPunch}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import {
|
|||||||
useParams,
|
useParams,
|
||||||
Link,
|
Link,
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
|
import { useApiMutation } from "../lib/queries/mutations";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
@@ -349,6 +350,7 @@ export default function InvoiceDetail() {
|
|||||||
{
|
{
|
||||||
_key: "inv-1",
|
_key: "inv-1",
|
||||||
description: "",
|
description: "",
|
||||||
|
item_description: "",
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
unit: "ks",
|
unit: "ks",
|
||||||
unit_price: 0,
|
unit_price: 0,
|
||||||
@@ -400,7 +402,6 @@ export default function InvoiceDetail() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ─── TanStack Query ───
|
// ─── TanStack Query ───
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const customersQuery = useQuery(offerCustomersOptions());
|
const customersQuery = useQuery(offerCustomersOptions());
|
||||||
const customers = customersQuery.data ?? [];
|
const customers = customersQuery.data ?? [];
|
||||||
@@ -762,6 +763,34 @@ export default function InvoiceDetail() {
|
|||||||
}, [items, form.apply_vat]);
|
}, [items, form.apply_vat]);
|
||||||
|
|
||||||
// ─── Create/Edit mode: submit ───
|
// ─── Create/Edit mode: submit ───
|
||||||
|
const saveMutation = useApiMutation<
|
||||||
|
Record<string, unknown>,
|
||||||
|
{ invoice_id: number }
|
||||||
|
>({
|
||||||
|
url: () => (isEdit ? `${API_BASE}/invoices/${id}` : `${API_BASE}/invoices`),
|
||||||
|
method: () => (isEdit ? "PUT" : "POST"),
|
||||||
|
invalidate: ["invoices", "orders"],
|
||||||
|
onSuccess: (data) => {
|
||||||
|
const invoiceId = isEdit ? Number(id) : data.invoice_id;
|
||||||
|
// PDF binary generation — KEEP as raw apiFetch
|
||||||
|
void apiFetch(
|
||||||
|
`${API_BASE}/invoices-pdf/${invoiceId}?lang=${form.language}&save=1`,
|
||||||
|
).catch(() => {});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusMutation = useApiMutation<{ status: string }, unknown>({
|
||||||
|
url: () => `${API_BASE}/invoices/${id}`,
|
||||||
|
method: () => "PUT",
|
||||||
|
invalidate: ["invoices", "orders"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const invoiceDeleteMutation = useApiMutation<void, unknown>({
|
||||||
|
url: () => `${API_BASE}/invoices/${id}`,
|
||||||
|
method: () => "DELETE",
|
||||||
|
invalidate: ["invoices", "orders"],
|
||||||
|
});
|
||||||
|
|
||||||
const handleCreateSubmit = async (e?: React.FormEvent) => {
|
const handleCreateSubmit = async (e?: React.FormEvent) => {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
|
|
||||||
@@ -779,7 +808,7 @@ export default function InvoiceDetail() {
|
|||||||
|
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const payload: any = {
|
const payload: Record<string, unknown> = {
|
||||||
...form,
|
...form,
|
||||||
due_date: computedDueDate || form.due_date,
|
due_date: computedDueDate || form.due_date,
|
||||||
items: items
|
items: items
|
||||||
@@ -791,48 +820,21 @@ export default function InvoiceDetail() {
|
|||||||
};
|
};
|
||||||
if (isEdit) payload.invoice_number = invoiceNumber;
|
if (isEdit) payload.invoice_number = invoiceNumber;
|
||||||
|
|
||||||
const url = isEdit
|
const data = await saveMutation.mutateAsync(payload);
|
||||||
? `${API_BASE}/invoices/${id}`
|
|
||||||
: `${API_BASE}/invoices`;
|
|
||||||
const method = isEdit ? "PUT" : "POST";
|
|
||||||
|
|
||||||
const response = await apiFetch(url, {
|
|
||||||
method,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
});
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
if (!isEdit) clearDraft();
|
if (!isEdit) clearDraft();
|
||||||
const invoiceId = isEdit ? id : result.data.invoice_id;
|
alert.success(isEdit ? "Faktura byla uložena" : "Faktura byla vytvořena");
|
||||||
await apiFetch(
|
|
||||||
`${API_BASE}/invoices-pdf/${invoiceId}?lang=${form.language}&save=1`,
|
|
||||||
).catch(() => {});
|
|
||||||
alert.success(
|
|
||||||
result.message ||
|
|
||||||
(isEdit ? "Faktura byla uložena" : "Faktura byla vytvořena"),
|
|
||||||
);
|
|
||||||
initialSnapshotRef.current = JSON.stringify({ form, items });
|
initialSnapshotRef.current = JSON.stringify({ form, items });
|
||||||
if (isEdit) {
|
if (!isEdit) {
|
||||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
navigate(`/invoices/${data.invoice_id}`);
|
||||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
|
||||||
} else {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
|
||||||
navigate(`/invoices/${result.data.invoice_id}`);
|
|
||||||
}
|
}
|
||||||
} else {
|
} catch (e) {
|
||||||
alert.error(
|
alert.error(
|
||||||
result.error ||
|
e instanceof Error
|
||||||
(isEdit
|
? e.message
|
||||||
|
: isEdit
|
||||||
? "Nepodařilo se uložit fakturu"
|
? "Nepodařilo se uložit fakturu"
|
||||||
: "Nepodařilo se vytvořit fakturu"),
|
: "Nepodařilo se vytvořit fakturu",
|
||||||
);
|
);
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -841,25 +843,14 @@ export default function InvoiceDetail() {
|
|||||||
// ─── Edit mode: status change ───
|
// ─── Edit mode: status change ───
|
||||||
const handleStatusChange = async () => {
|
const handleStatusChange = async () => {
|
||||||
if (!statusConfirm.status) return;
|
if (!statusConfirm.status) return;
|
||||||
setStatusChanging(statusConfirm.status);
|
const newStatus = statusConfirm.status;
|
||||||
|
setStatusChanging(newStatus);
|
||||||
setStatusConfirm({ show: false, status: null });
|
setStatusConfirm({ show: false, status: null });
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(`${API_BASE}/invoices/${id}`, {
|
await statusMutation.mutateAsync({ status: newStatus });
|
||||||
method: "PUT",
|
alert.success("Stav byl změněn");
|
||||||
headers: { "Content-Type": "application/json" },
|
} catch (e) {
|
||||||
body: JSON.stringify({ status: statusConfirm.status }),
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
});
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
alert.success(result.message || "Stav byl změněn");
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
|
||||||
} else {
|
|
||||||
alert.error(result.error || "Nepodařilo se změnit stav");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
} finally {
|
||||||
setStatusChanging(null);
|
setStatusChanging(null);
|
||||||
}
|
}
|
||||||
@@ -893,21 +884,11 @@ export default function InvoiceDetail() {
|
|||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
setDeleting(true);
|
setDeleting(true);
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(`${API_BASE}/invoices/${id}`, {
|
await invoiceDeleteMutation.mutateAsync(undefined);
|
||||||
method: "DELETE",
|
alert.success("Faktura byla smazána");
|
||||||
});
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
alert.success(result.message || "Faktura byla smazána");
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
|
||||||
navigate("/invoices");
|
navigate("/invoices");
|
||||||
} else {
|
} catch (e) {
|
||||||
alert.error(result.error || "Nepodařilo se smazat fakturu");
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
} finally {
|
||||||
setDeleting(false);
|
setDeleting(false);
|
||||||
setDeleteConfirm(false);
|
setDeleteConfirm(false);
|
||||||
@@ -1101,7 +1082,7 @@ export default function InvoiceDetail() {
|
|||||||
<div className="admin-card-header flex-between">
|
<div className="admin-card-header flex-between">
|
||||||
<h3 className="admin-card-title">Položky</h3>
|
<h3 className="admin-card-title">Položky</h3>
|
||||||
</div>
|
</div>
|
||||||
{invoice.items?.length > 0 ? (
|
{invoice.items && invoice.items.length > 0 ? (
|
||||||
<div className="admin-table-responsive">
|
<div className="admin-table-responsive">
|
||||||
<table className="admin-table">
|
<table className="admin-table">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { formatDate, formatDatetime } from "../utils/attendanceHelpers";
|
import { formatDate, formatDatetime } from "../utils/attendanceHelpers";
|
||||||
import apiFetch from "../utils/api";
|
|
||||||
import { czechPlural } from "../utils/formatters";
|
import { czechPlural } from "../utils/formatters";
|
||||||
import {
|
import {
|
||||||
leavePendingOptions,
|
leavePendingOptions,
|
||||||
@@ -12,10 +11,9 @@ import {
|
|||||||
} from "../lib/queries/leave";
|
} from "../lib/queries/leave";
|
||||||
import ConfirmModal from "../components/ConfirmModal";
|
import ConfirmModal from "../components/ConfirmModal";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import useModalLock from "../hooks/useModalLock";
|
import FormModal from "../components/FormModal";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
import { Skeleton } from "boneyard-js/react";
|
import { useApiMutation } from "../lib/queries/mutations";
|
||||||
import LeaveApprovalFixture from "../fixtures/LeaveApprovalFixture";
|
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
@@ -108,7 +106,6 @@ function mapLeaveRequest(raw: RawLeaveRequest): LeaveRequest {
|
|||||||
export default function LeaveApproval() {
|
export default function LeaveApproval() {
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const [activeTab, setActiveTab] = useState<"pending" | "processed">(
|
const [activeTab, setActiveTab] = useState<"pending" | "processed">(
|
||||||
"pending",
|
"pending",
|
||||||
);
|
);
|
||||||
@@ -135,39 +132,45 @@ export default function LeaveApproval() {
|
|||||||
request: LeaveRequest | null;
|
request: LeaveRequest | null;
|
||||||
}>({ open: false, request: null });
|
}>({ open: false, request: null });
|
||||||
const [rejectNote, setRejectNote] = useState("");
|
const [rejectNote, setRejectNote] = useState("");
|
||||||
const [processing, setProcessing] = useState(false);
|
|
||||||
|
|
||||||
useModalLock(rejectModal.open);
|
|
||||||
|
|
||||||
if (!hasPermission("attendance.approve")) return <Forbidden />;
|
if (!hasPermission("attendance.approve")) return <Forbidden />;
|
||||||
|
|
||||||
const handleApprove = async () => {
|
const approveMutation = useApiMutation<
|
||||||
setProcessing(true);
|
{ id: number; status: "approved" },
|
||||||
try {
|
unknown
|
||||||
const response = await apiFetch(
|
>({
|
||||||
`${API_BASE}/leave-requests/${approveModal.request!.id}`,
|
url: ({ id }) => `${API_BASE}/leave-requests/${id}`,
|
||||||
{
|
method: () => "PUT",
|
||||||
method: "PUT",
|
invalidate: ["leave-requests", "leave", "attendance", "users"],
|
||||||
headers: { "Content-Type": "application/json" },
|
onSuccess: () => {
|
||||||
body: JSON.stringify({ status: "approved" }),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (response.status === 401) return;
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setApproveModal({ open: false, request: null });
|
setApproveModal({ open: false, request: null });
|
||||||
await queryClient.invalidateQueries({ queryKey: ["leave"] });
|
|
||||||
await queryClient.invalidateQueries({ queryKey: ["leave-requests"] });
|
|
||||||
await queryClient.invalidateQueries({ queryKey: ["attendance"] });
|
|
||||||
alert.success("Žádost byla schválena");
|
alert.success("Žádost byla schválena");
|
||||||
} else {
|
},
|
||||||
alert.error(result.error);
|
});
|
||||||
}
|
|
||||||
} catch {
|
const rejectMutation = useApiMutation<
|
||||||
alert.error("Chyba připojení");
|
{ id: number; status: "rejected"; reviewer_note: string },
|
||||||
} finally {
|
unknown
|
||||||
setProcessing(false);
|
>({
|
||||||
|
url: ({ id }) => `${API_BASE}/leave-requests/${id}`,
|
||||||
|
method: () => "PUT",
|
||||||
|
invalidate: ["leave-requests", "leave", "attendance", "users"],
|
||||||
|
onSuccess: () => {
|
||||||
|
setRejectModal({ open: false, request: null });
|
||||||
|
setRejectNote("");
|
||||||
|
alert.success("Žádost byla zamítnuta");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleApprove = async () => {
|
||||||
|
if (!approveModal.request) return;
|
||||||
|
try {
|
||||||
|
await approveMutation.mutateAsync({
|
||||||
|
id: approveModal.request.id,
|
||||||
|
status: "approved",
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -176,46 +179,27 @@ export default function LeaveApproval() {
|
|||||||
alert.error("Důvod zamítnutí je povinný");
|
alert.error("Důvod zamítnutí je povinný");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!rejectModal.request) return;
|
||||||
setProcessing(true);
|
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(
|
await rejectMutation.mutateAsync({
|
||||||
`${API_BASE}/leave-requests/${rejectModal.request!.id}`,
|
id: rejectModal.request.id,
|
||||||
{
|
|
||||||
method: "PUT",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
status: "rejected",
|
status: "rejected",
|
||||||
reviewer_note: rejectNote,
|
reviewer_note: rejectNote,
|
||||||
}),
|
});
|
||||||
},
|
} catch (e) {
|
||||||
);
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
if (response.status === 401) return;
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setRejectModal({ open: false, request: null });
|
|
||||||
setRejectNote("");
|
|
||||||
await queryClient.invalidateQueries({ queryKey: ["leave"] });
|
|
||||||
await queryClient.invalidateQueries({ queryKey: ["leave-requests"] });
|
|
||||||
await queryClient.invalidateQueries({ queryKey: ["attendance"] });
|
|
||||||
alert.success("Žádost byla zamítnuta");
|
|
||||||
} else {
|
|
||||||
alert.error(result.error);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
|
||||||
setProcessing(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="admin-loading">
|
||||||
|
<div className="admin-spinner" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Skeleton
|
|
||||||
name="leave-approval"
|
|
||||||
loading={loading}
|
|
||||||
fixture={<LeaveApprovalFixture />}
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="admin-page-header"
|
className="admin-page-header"
|
||||||
@@ -325,8 +309,7 @@ export default function LeaveApproval() {
|
|||||||
<span
|
<span
|
||||||
className={`attendance-leave-badge ${leaveTypeClasses[req.leave_type] || ""}`}
|
className={`attendance-leave-badge ${leaveTypeClasses[req.leave_type] || ""}`}
|
||||||
>
|
>
|
||||||
{leaveTypeLabels[req.leave_type] ||
|
{leaveTypeLabels[req.leave_type] || req.leave_type}
|
||||||
req.leave_type}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -344,8 +327,8 @@ export default function LeaveApproval() {
|
|||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{req.total_days}{" "}
|
{req.total_days}{" "}
|
||||||
{czechPlural(req.total_days, "den", "dny", "dnů")}{" "}
|
{czechPlural(req.total_days, "den", "dny", "dnů")} (
|
||||||
({req.total_hours}h)
|
{req.total_hours}h)
|
||||||
</span>
|
</span>
|
||||||
<span className="text-muted">
|
<span className="text-muted">
|
||||||
Podáno: {formatDatetime(req.created_at)}
|
Podáno: {formatDatetime(req.created_at)}
|
||||||
@@ -446,8 +429,7 @@ export default function LeaveApproval() {
|
|||||||
<span
|
<span
|
||||||
className={`attendance-leave-badge ${leaveTypeClasses[req.leave_type] || ""}`}
|
className={`attendance-leave-badge ${leaveTypeClasses[req.leave_type] || ""}`}
|
||||||
>
|
>
|
||||||
{leaveTypeLabels[req.leave_type] ||
|
{leaveTypeLabels[req.leave_type] || req.leave_type}
|
||||||
req.leave_type}
|
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="admin-mono">
|
<td className="admin-mono">
|
||||||
@@ -505,38 +487,23 @@ export default function LeaveApproval() {
|
|||||||
}
|
}
|
||||||
confirmText="Schválit"
|
confirmText="Schválit"
|
||||||
type="info"
|
type="info"
|
||||||
loading={processing}
|
loading={approveMutation.isPending}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Reject Modal */}
|
{/* Reject Modal */}
|
||||||
<AnimatePresence>
|
<FormModal
|
||||||
{rejectModal.open && (
|
isOpen={rejectModal.open && !!rejectModal.request}
|
||||||
<motion.div
|
onClose={() => {
|
||||||
className="admin-modal-overlay"
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="admin-modal-backdrop"
|
|
||||||
onClick={() => {
|
|
||||||
setRejectModal({ open: false, request: null });
|
setRejectModal({ open: false, request: null });
|
||||||
setRejectNote("");
|
setRejectNote("");
|
||||||
}}
|
}}
|
||||||
/>
|
onSubmit={handleReject}
|
||||||
<motion.div
|
title="Zamítnout žádost"
|
||||||
className="admin-modal"
|
submitLabel="Zamítnout"
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
loading={rejectMutation.isPending}
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
>
|
||||||
<div className="admin-modal-header">
|
|
||||||
<h2 className="admin-modal-title">Zamítnout žádost</h2>
|
|
||||||
</div>
|
|
||||||
<div className="admin-modal-body">
|
|
||||||
{rejectModal.request && (
|
{rejectModal.request && (
|
||||||
|
<>
|
||||||
<p className="text-secondary mb-4">
|
<p className="text-secondary mb-4">
|
||||||
{rejectModal.request.employee_name} —{" "}
|
{rejectModal.request.employee_name} —{" "}
|
||||||
{leaveTypeLabels[rejectModal.request.leave_type]},{" "}
|
{leaveTypeLabels[rejectModal.request.leave_type]},{" "}
|
||||||
@@ -544,7 +511,6 @@ export default function LeaveApproval() {
|
|||||||
{formatDate(rejectModal.request.date_to)} (
|
{formatDate(rejectModal.request.date_to)} (
|
||||||
{rejectModal.request.total_days} dnů)
|
{rejectModal.request.total_days} dnů)
|
||||||
</p>
|
</p>
|
||||||
)}
|
|
||||||
<FormField label="Důvod zamítnutí" required>
|
<FormField label="Důvod zamítnutí" required>
|
||||||
<textarea
|
<textarea
|
||||||
value={rejectNote}
|
value={rejectNote}
|
||||||
@@ -555,33 +521,9 @@ export default function LeaveApproval() {
|
|||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</>
|
||||||
<div className="admin-modal-footer">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setRejectModal({ open: false, request: null });
|
|
||||||
setRejectNote("");
|
|
||||||
}}
|
|
||||||
className="admin-btn admin-btn-secondary"
|
|
||||||
disabled={processing}
|
|
||||||
>
|
|
||||||
Zrušit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleReject}
|
|
||||||
disabled={processing || !rejectNote.trim()}
|
|
||||||
className="admin-btn admin-btn-primary"
|
|
||||||
>
|
|
||||||
{processing ? "Zpracování..." : "Zamítnout"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</FormModal>
|
||||||
</div>
|
</div>
|
||||||
</Skeleton>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
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 { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import { Skeleton } from "boneyard-js/react";
|
|
||||||
import LeaveRequestsFixture from "../fixtures/LeaveRequestsFixture";
|
|
||||||
import { formatDate, formatDatetime } from "../utils/attendanceHelpers";
|
import { formatDate, formatDatetime } from "../utils/attendanceHelpers";
|
||||||
import apiFetch from "../utils/api";
|
|
||||||
import ConfirmModal from "../components/ConfirmModal";
|
import ConfirmModal from "../components/ConfirmModal";
|
||||||
import { leaveRequestsOptions, type LeaveRequest } from "../lib/queries/leave";
|
import { leaveRequestsOptions, type LeaveRequest } from "../lib/queries/leave";
|
||||||
|
import { useApiMutation } from "../lib/queries/mutations";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
@@ -42,7 +40,6 @@ const leaveTypeClasses: Record<string, string> = {
|
|||||||
export default function LeaveRequests() {
|
export default function LeaveRequests() {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const { data: requests = [], isPending } = useQuery(
|
const { data: requests = [], isPending } = useQuery(
|
||||||
leaveRequestsOptions(true),
|
leaveRequestsOptions(true),
|
||||||
);
|
);
|
||||||
@@ -50,35 +47,28 @@ export default function LeaveRequests() {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
id: number | null;
|
id: number | null;
|
||||||
}>({ open: false, id: null });
|
}>({ open: false, id: null });
|
||||||
const [cancelling, setCancelling] = useState(false);
|
|
||||||
|
const cancelMutation = useApiMutation<
|
||||||
|
{ id: number },
|
||||||
|
{ message?: string; error?: string }
|
||||||
|
>({
|
||||||
|
url: ({ id }) => `${API_BASE}/leave-requests/${id}`,
|
||||||
|
method: () => "DELETE",
|
||||||
|
invalidate: ["leave-requests", "leave", "attendance"],
|
||||||
|
onSuccess: (data) => {
|
||||||
|
setCancelModal({ open: false, id: null });
|
||||||
|
alert.success(data?.message || "Žádost byla zrušena");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (!hasPermission("attendance.record")) return <Forbidden />;
|
if (!hasPermission("attendance.record")) return <Forbidden />;
|
||||||
|
|
||||||
const handleCancel = async () => {
|
const handleCancel = async () => {
|
||||||
setCancelling(true);
|
if (!cancelModal.id) return;
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(
|
await cancelMutation.mutateAsync({ id: cancelModal.id });
|
||||||
`${API_BASE}/leave-requests/${cancelModal.id}`,
|
} catch (e) {
|
||||||
{
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
method: "DELETE",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (response.status === 401) return;
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setCancelModal({ open: false, id: null });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["leave-requests"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["leave"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["attendance"] });
|
|
||||||
alert.success(result.message);
|
|
||||||
} else {
|
|
||||||
alert.error(result.error);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
|
||||||
setCancelling(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -109,12 +99,15 @@ export default function LeaveRequests() {
|
|||||||
return <span className="text-muted">—</span>;
|
return <span className="text-muted">—</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isPending) {
|
||||||
|
return (
|
||||||
|
<div className="admin-loading">
|
||||||
|
<div className="admin-spinner" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Skeleton
|
|
||||||
name="leave-requests"
|
|
||||||
loading={isPending}
|
|
||||||
fixture={<LeaveRequestsFixture />}
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="admin-page-header"
|
className="admin-page-header"
|
||||||
@@ -124,9 +117,7 @@ export default function LeaveRequests() {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="admin-page-title">Moje žádosti</h1>
|
<h1 className="admin-page-title">Moje žádosti</h1>
|
||||||
<p className="admin-page-subtitle">
|
<p className="admin-page-subtitle">Přehled žádostí o nepřítomnost</p>
|
||||||
Přehled žádostí o nepřítomnost
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@@ -174,7 +165,7 @@ export default function LeaveRequests() {
|
|||||||
<th>Stav</th>
|
<th>Stav</th>
|
||||||
<th>Poznámka</th>
|
<th>Poznámka</th>
|
||||||
<th>Podáno</th>
|
<th>Podáno</th>
|
||||||
<th></th>
|
<th>Akce</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -190,9 +181,7 @@ export default function LeaveRequests() {
|
|||||||
<td className="admin-mono">
|
<td className="admin-mono">
|
||||||
{formatDate(req.date_from)}
|
{formatDate(req.date_from)}
|
||||||
</td>
|
</td>
|
||||||
<td className="admin-mono">
|
<td className="admin-mono">{formatDate(req.date_to)}</td>
|
||||||
{formatDate(req.date_to)}
|
|
||||||
</td>
|
|
||||||
<td className="admin-mono">{req.total_days}</td>
|
<td className="admin-mono">{req.total_days}</td>
|
||||||
<td className="admin-mono">{req.total_hours}h</td>
|
<td className="admin-mono">{req.total_hours}h</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -212,6 +201,7 @@ export default function LeaveRequests() {
|
|||||||
{formatDatetime(req.created_at)}
|
{formatDatetime(req.created_at)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
<div className="admin-table-actions">
|
||||||
{req.status === "pending" && (
|
{req.status === "pending" && (
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -222,6 +212,7 @@ export default function LeaveRequests() {
|
|||||||
Zrušit
|
Zrušit
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@@ -240,9 +231,8 @@ export default function LeaveRequests() {
|
|||||||
message="Opravdu chcete zrušit tuto žádost o nepřítomnost?"
|
message="Opravdu chcete zrušit tuto žádost o nepřítomnost?"
|
||||||
confirmText="Zrušit žádost"
|
confirmText="Zrušit žádost"
|
||||||
type="warning"
|
type="warning"
|
||||||
loading={cancelling}
|
loading={cancelMutation.isPending}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Skeleton>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,11 +49,11 @@ import {
|
|||||||
} from "@dnd-kit/modifiers";
|
} from "@dnd-kit/modifiers";
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
import ConfirmModal from "../components/ConfirmModal";
|
import ConfirmModal from "../components/ConfirmModal";
|
||||||
|
import FormModal from "../components/FormModal";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import AdminDatePicker from "../components/AdminDatePicker";
|
import AdminDatePicker from "../components/AdminDatePicker";
|
||||||
import RichEditor from "../components/RichEditor";
|
import RichEditor from "../components/RichEditor";
|
||||||
import useModalLock from "../hooks/useModalLock";
|
|
||||||
import useDebounce from "../hooks/useDebounce";
|
import useDebounce from "../hooks/useDebounce";
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
import { formatCurrency } from "../utils/formatters";
|
import { formatCurrency } from "../utils/formatters";
|
||||||
@@ -61,6 +61,7 @@ import {
|
|||||||
companySettingsOptions,
|
companySettingsOptions,
|
||||||
type CompanySettingsData,
|
type CompanySettingsData,
|
||||||
} from "../lib/queries/settings";
|
} from "../lib/queries/settings";
|
||||||
|
import { useApiMutation } from "../lib/queries/mutations";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
const DRAFT_KEY = "boha_offer_draft";
|
const DRAFT_KEY = "boha_offer_draft";
|
||||||
@@ -403,9 +404,13 @@ export default function OfferDetail() {
|
|||||||
const heartbeatRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
const heartbeatRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
const unlockAbortRef = useRef<AbortController | null>(null);
|
const unlockAbortRef = useRef<AbortController | null>(null);
|
||||||
const initialSnapshotRef = useRef<string | null>(null);
|
const initialSnapshotRef = useRef<string | null>(null);
|
||||||
|
// Set to true right after a successful delete so the detail-page error
|
||||||
|
// effect doesn't fire "Nepodařilo se načíst nabídku" on top of the
|
||||||
|
// success toast (the 404 from a refetched-already-deleted row is
|
||||||
|
// expected here, not a real error).
|
||||||
|
const deletingRef = useRef(false);
|
||||||
|
|
||||||
useModalLock(showOrderModal);
|
// Note: FormModal applies useModalLock internally.
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
blobTimeoutsRef.current.forEach(clearTimeout);
|
blobTimeoutsRef.current.forEach(clearTimeout);
|
||||||
@@ -476,9 +481,12 @@ export default function OfferDetail() {
|
|||||||
formInitializedRef.current = true;
|
formInitializedRef.current = true;
|
||||||
}, [offerQuery.data, companySettings, hasPermission, id, emptyItem]);
|
}, [offerQuery.data, companySettings, hasPermission, id, emptyItem]);
|
||||||
|
|
||||||
// Redirect on offer fetch error (edit mode)
|
// Redirect on offer fetch error (edit mode).
|
||||||
|
// Suppress when deletingRef is set: the in-flight refetch of a row we
|
||||||
|
// just deleted will 404, and we don't want a "load failed" toast on
|
||||||
|
// top of the success toast.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEdit && offerQuery.isError) {
|
if (isEdit && offerQuery.isError && !deletingRef.current) {
|
||||||
alert.error("Nepodařilo se načíst nabídku");
|
alert.error("Nepodařilo se načíst nabídku");
|
||||||
navigate("/offers");
|
navigate("/offers");
|
||||||
}
|
}
|
||||||
@@ -670,7 +678,7 @@ export default function OfferDetail() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isEdit && result.data?.id) {
|
if (!isEdit && result.data?.id) {
|
||||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
queryClient.invalidateQueries({ queryKey: ["offers", "list"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||||
@@ -682,7 +690,7 @@ export default function OfferDetail() {
|
|||||||
items,
|
items,
|
||||||
sections,
|
sections,
|
||||||
});
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
queryClient.invalidateQueries({ queryKey: ["offers", "list"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||||
@@ -713,7 +721,7 @@ export default function OfferDetail() {
|
|||||||
formData.append("attachment", orderAttachment);
|
formData.append("attachment", orderAttachment);
|
||||||
fetchOptions = { method: "POST", body: formData };
|
fetchOptions = { method: "POST", body: formData };
|
||||||
} else {
|
} else {
|
||||||
// Without attachment: send as JSON (avoids multipart content-type issues)
|
// Without attachment: send as JSON
|
||||||
fetchOptions = {
|
fetchOptions = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@@ -728,7 +736,7 @@ export default function OfferDetail() {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
setShowOrderModal(false);
|
setShowOrderModal(false);
|
||||||
alert.success(result.message || "Objednávka byla vytvořena");
|
alert.success(result.message || "Objednávka byla vytvořena");
|
||||||
await queryClient.invalidateQueries({ queryKey: ["offers"] });
|
await queryClient.invalidateQueries({ queryKey: ["offers", "list"] });
|
||||||
await queryClient.invalidateQueries({ queryKey: ["orders"] });
|
await queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
await queryClient.invalidateQueries({ queryKey: ["projects"] });
|
await queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||||
await queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
await queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||||
@@ -754,7 +762,7 @@ export default function OfferDetail() {
|
|||||||
setInvalidateConfirm(false);
|
setInvalidateConfirm(false);
|
||||||
setOfferStatus("invalidated");
|
setOfferStatus("invalidated");
|
||||||
alert.success(result.message || "Nabídka byla zneplatněna");
|
alert.success(result.message || "Nabídka byla zneplatněna");
|
||||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
queryClient.invalidateQueries({ queryKey: ["offers", "list"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||||
@@ -776,8 +784,12 @@ export default function OfferDetail() {
|
|||||||
});
|
});
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
// Mark the row as gone before the list invalidation refetches the
|
||||||
|
// detail key (which would 404 and trigger the error-toast effect).
|
||||||
|
deletingRef.current = true;
|
||||||
|
queryClient.removeQueries({ queryKey: ["offers", id] });
|
||||||
alert.success(result.message || "Nabídka byla smazána");
|
alert.success(result.message || "Nabídka byla smazána");
|
||||||
await queryClient.invalidateQueries({ queryKey: ["offers"] });
|
await queryClient.invalidateQueries({ queryKey: ["offers", "list"] });
|
||||||
await queryClient.invalidateQueries({ queryKey: ["orders"] });
|
await queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||||
await queryClient.invalidateQueries({ queryKey: ["projects"] });
|
await queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||||
await queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
await queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||||
@@ -1374,7 +1386,7 @@ export default function OfferDetail() {
|
|||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
canDelete={items.length > 1}
|
canDelete={items.length > 1}
|
||||||
onUpdate={(field, value) =>
|
onUpdate={(field, value) =>
|
||||||
updateItem(index, field, value)
|
updateItem(index, field as keyof OfferItem, value)
|
||||||
}
|
}
|
||||||
onRemove={() => removeItem(index)}
|
onRemove={() => removeItem(index)}
|
||||||
/>
|
/>
|
||||||
@@ -1678,30 +1690,14 @@ export default function OfferDetail() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Order modal */}
|
{/* Order modal */}
|
||||||
<AnimatePresence>
|
<FormModal
|
||||||
{showOrderModal && (
|
isOpen={showOrderModal}
|
||||||
<motion.div
|
onClose={() => !creatingOrder && setShowOrderModal(false)}
|
||||||
className="admin-modal-overlay"
|
onSubmit={handleCreateOrder}
|
||||||
initial={{ opacity: 0 }}
|
title="Vytvořit objednávku"
|
||||||
animate={{ opacity: 1 }}
|
submitLabel={creatingOrder ? "Vytváření..." : "Vytvořit"}
|
||||||
exit={{ opacity: 0 }}
|
loading={creatingOrder}
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
className="admin-modal-backdrop"
|
|
||||||
onClick={() => !creatingOrder && setShowOrderModal(false)}
|
|
||||||
/>
|
|
||||||
<motion.div
|
|
||||||
className="admin-modal"
|
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
|
||||||
<div className="admin-modal-header">
|
|
||||||
<h2 className="admin-modal-title">Vytvořit objednávku</h2>
|
|
||||||
</div>
|
|
||||||
<div className="admin-modal-body">
|
|
||||||
<div className="admin-form">
|
<div className="admin-form">
|
||||||
<FormField label="Číslo objednávky zákazníka" required>
|
<FormField label="Číslo objednávky zákazníka" required>
|
||||||
<input
|
<input
|
||||||
@@ -1711,9 +1707,7 @@ export default function OfferDetail() {
|
|||||||
setCustomerOrderNumber(e.target.value)
|
setCustomerOrderNumber(e.target.value)
|
||||||
}
|
}
|
||||||
onKeyDown={(e) =>
|
onKeyDown={(e) =>
|
||||||
e.key === "Enter" &&
|
e.key === "Enter" && !creatingOrder && handleCreateOrder()
|
||||||
!creatingOrder &&
|
|
||||||
handleCreateOrder()
|
|
||||||
}
|
}
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
placeholder="Např. PO-2026-001"
|
placeholder="Např. PO-2026-001"
|
||||||
@@ -1769,35 +1763,12 @@ export default function OfferDetail() {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
<small
|
<small className="admin-form-hint" style={{ marginTop: "0.25rem" }}>
|
||||||
className="admin-form-hint"
|
|
||||||
style={{ marginTop: "0.25rem" }}
|
|
||||||
>
|
|
||||||
Max 10 MB
|
Max 10 MB
|
||||||
</small>
|
</small>
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</FormModal>
|
||||||
<div className="admin-modal-footer">
|
|
||||||
<button
|
|
||||||
onClick={() => setShowOrderModal(false)}
|
|
||||||
className="admin-btn admin-btn-secondary"
|
|
||||||
disabled={creatingOrder}
|
|
||||||
>
|
|
||||||
Zrušit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleCreateOrder}
|
|
||||||
className="admin-btn admin-btn-primary"
|
|
||||||
disabled={creatingOrder || !customerOrderNumber.trim()}
|
|
||||||
>
|
|
||||||
{creatingOrder ? "Vytváření..." : "Vytvořit"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={invalidateConfirm}
|
isOpen={invalidateConfirm}
|
||||||
|
|||||||
@@ -4,18 +4,19 @@ import { useAuth } from "../context/AuthContext";
|
|||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import ConfirmModal from "../components/ConfirmModal";
|
import ConfirmModal from "../components/ConfirmModal";
|
||||||
|
import FormModal from "../components/FormModal";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
|
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
import { formatCurrency, formatDate, czechPlural } from "../utils/formatters";
|
import { formatCurrency, 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 { useQueryClient, useQuery } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { usePaginatedQuery } from "../hooks/usePaginatedQuery";
|
import { usePaginatedQuery } from "../hooks/usePaginatedQuery";
|
||||||
import { offerListOptions, offerCustomersOptions } from "../lib/queries/offers";
|
import { offerListOptions, offerCustomersOptions } from "../lib/queries/offers";
|
||||||
import useModalLock from "../hooks/useModalLock";
|
|
||||||
import Pagination from "../components/Pagination";
|
import Pagination from "../components/Pagination";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
|
import { useApiMutation } from "../lib/queries/mutations";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
const DRAFT_KEY = "boha_offer_draft";
|
const DRAFT_KEY = "boha_offer_draft";
|
||||||
@@ -27,12 +28,6 @@ const STATUS_FILTERS = [
|
|||||||
{ value: "invalidated", label: "Zneplatněná" },
|
{ value: "invalidated", label: "Zneplatněná" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const ORDER_FILTERS = [
|
|
||||||
{ value: "", label: "Vše" },
|
|
||||||
{ value: "yes", label: "S objednávkou" },
|
|
||||||
{ value: "no", label: "Bez objednávky" },
|
|
||||||
];
|
|
||||||
|
|
||||||
interface Quotation {
|
interface Quotation {
|
||||||
id: number;
|
id: number;
|
||||||
quotation_number: string;
|
quotation_number: string;
|
||||||
@@ -70,7 +65,6 @@ export default function Offers() {
|
|||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [statusFilter, setStatusFilter] = useState("");
|
const [statusFilter, setStatusFilter] = useState("");
|
||||||
const [customerFilter, setCustomerFilter] = useState<number | "">("");
|
const [customerFilter, setCustomerFilter] = useState<number | "">("");
|
||||||
const [orderFilter, setOrderFilter] = useState("");
|
|
||||||
|
|
||||||
const { data: customers } = useQuery(offerCustomersOptions());
|
const { data: customers } = useQuery(offerCustomersOptions());
|
||||||
|
|
||||||
@@ -101,7 +95,6 @@ export default function Offers() {
|
|||||||
show: boolean;
|
show: boolean;
|
||||||
quotation: Quotation | null;
|
quotation: Quotation | null;
|
||||||
}>({ show: false, quotation: null });
|
}>({ show: false, quotation: null });
|
||||||
useModalLock(orderModal.show);
|
|
||||||
const [customerOrderNumber, setCustomerOrderNumber] = useState("");
|
const [customerOrderNumber, setCustomerOrderNumber] = useState("");
|
||||||
const [orderAttachment, setOrderAttachment] = useState<File | null>(null);
|
const [orderAttachment, setOrderAttachment] = useState<File | null>(null);
|
||||||
const [draft, setDraft] = useState<Draft | null>(() => {
|
const [draft, setDraft] = useState<Draft | null>(() => {
|
||||||
@@ -130,10 +123,47 @@ export default function Offers() {
|
|||||||
page,
|
page,
|
||||||
status: statusFilter || undefined,
|
status: statusFilter || undefined,
|
||||||
customer_id: customerFilter || undefined,
|
customer_id: customerFilter || undefined,
|
||||||
has_order: orderFilter || undefined,
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const duplicateMutation = useApiMutation<
|
||||||
|
number,
|
||||||
|
{ message?: string; error?: string }
|
||||||
|
>({
|
||||||
|
url: (id) => `${API_BASE}/offers/${id}/duplicate`,
|
||||||
|
method: () => "POST",
|
||||||
|
invalidate: ["offers", "orders", "projects", "invoices"],
|
||||||
|
onSuccess: (data) => {
|
||||||
|
alert.success(data?.message || "Nabídka byla duplikována");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteOfferMutation = useApiMutation<
|
||||||
|
number,
|
||||||
|
{ message?: string; error?: string }
|
||||||
|
>({
|
||||||
|
url: (id) => `${API_BASE}/offers/${id}`,
|
||||||
|
method: () => "DELETE",
|
||||||
|
invalidate: ["offers", "orders", "projects", "invoices"],
|
||||||
|
onSuccess: (data) => {
|
||||||
|
setDeleteConfirm({ show: false, quotation: null });
|
||||||
|
alert.success(data?.message || "Nabídka byla smazána");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const invalidateMutation = useApiMutation<
|
||||||
|
number,
|
||||||
|
{ message?: string; error?: string }
|
||||||
|
>({
|
||||||
|
url: (id) => `${API_BASE}/offers/${id}/invalidate`,
|
||||||
|
method: () => "POST",
|
||||||
|
invalidate: ["offers", "orders", "projects", "invoices"],
|
||||||
|
onSuccess: (data) => {
|
||||||
|
setInvalidateConfirm({ show: false, quotation: null });
|
||||||
|
alert.success(data?.message || "Nabídka byla zneplatněna");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const discardDraft = () => {
|
const discardDraft = () => {
|
||||||
try {
|
try {
|
||||||
localStorage.removeItem(DRAFT_KEY);
|
localStorage.removeItem(DRAFT_KEY);
|
||||||
@@ -159,25 +189,9 @@ export default function Offers() {
|
|||||||
const handleDuplicate = async (quotation: Quotation) => {
|
const handleDuplicate = async (quotation: Quotation) => {
|
||||||
setDuplicating(quotation.id);
|
setDuplicating(quotation.id);
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(
|
await duplicateMutation.mutateAsync(quotation.id);
|
||||||
`${API_BASE}/offers/${quotation.id}/duplicate`,
|
} catch (e) {
|
||||||
{
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
alert.success(result.message || "Nabídka byla duplikována");
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
|
||||||
} else {
|
|
||||||
alert.error(result.error || "Nepodařilo se duplikovat nabídku");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
} finally {
|
||||||
setDuplicating(null);
|
setDuplicating(null);
|
||||||
}
|
}
|
||||||
@@ -187,16 +201,25 @@ export default function Offers() {
|
|||||||
if (!customerOrderNumber.trim() || !orderModal.quotation) return;
|
if (!customerOrderNumber.trim() || !orderModal.quotation) return;
|
||||||
setCreatingOrder(orderModal.quotation.id);
|
setCreatingOrder(orderModal.quotation.id);
|
||||||
try {
|
try {
|
||||||
|
let fetchOptions: RequestInit;
|
||||||
|
if (orderAttachment) {
|
||||||
|
// With attachment: send as multipart/form-data
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("quotationId", String(orderModal.quotation.id));
|
formData.append("quotationId", String(orderModal.quotation.id));
|
||||||
formData.append("customerOrderNumber", customerOrderNumber.trim());
|
formData.append("customerOrderNumber", customerOrderNumber.trim());
|
||||||
if (orderAttachment) {
|
|
||||||
formData.append("attachment", orderAttachment);
|
formData.append("attachment", orderAttachment);
|
||||||
}
|
fetchOptions = { method: "POST", body: formData };
|
||||||
const response = await apiFetch(`${API_BASE}/orders`, {
|
} else {
|
||||||
|
fetchOptions = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
headers: { "Content-Type": "application/json" },
|
||||||
});
|
body: JSON.stringify({
|
||||||
|
quotationId: orderModal.quotation.id,
|
||||||
|
customerOrderNumber: customerOrderNumber.trim(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const response = await apiFetch(`${API_BASE}/orders`, fetchOptions);
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setOrderModal({ show: false, quotation: null });
|
setOrderModal({ show: false, quotation: null });
|
||||||
@@ -220,25 +243,9 @@ export default function Offers() {
|
|||||||
if (!deleteConfirm.quotation) return;
|
if (!deleteConfirm.quotation) return;
|
||||||
setDeleting(true);
|
setDeleting(true);
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(
|
await deleteOfferMutation.mutateAsync(deleteConfirm.quotation.id);
|
||||||
`${API_BASE}/offers/${deleteConfirm.quotation.id}`,
|
} catch (e) {
|
||||||
{
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
method: "DELETE",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setDeleteConfirm({ show: false, quotation: null });
|
|
||||||
alert.success(result.message || "Nabídka byla smazána");
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
|
||||||
} else {
|
|
||||||
alert.error(result.error || "Nepodařilo se smazat nabídku");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
} finally {
|
||||||
setDeleting(false);
|
setDeleting(false);
|
||||||
}
|
}
|
||||||
@@ -248,25 +255,9 @@ export default function Offers() {
|
|||||||
if (!invalidateConfirm.quotation) return;
|
if (!invalidateConfirm.quotation) return;
|
||||||
setInvalidating(true);
|
setInvalidating(true);
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(
|
await invalidateMutation.mutateAsync(invalidateConfirm.quotation.id);
|
||||||
`${API_BASE}/offers/${invalidateConfirm.quotation.id}/invalidate`,
|
} catch (e) {
|
||||||
{
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
method: "POST",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setInvalidateConfirm({ show: false, quotation: null });
|
|
||||||
alert.success(result.message || "Nabídka byla zneplatněna");
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
|
||||||
} else {
|
|
||||||
alert.error(result.error || "Nepodařilo se zneplatnit nabídku");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
} finally {
|
||||||
setInvalidating(false);
|
setInvalidating(false);
|
||||||
}
|
}
|
||||||
@@ -433,28 +424,11 @@ export default function Offers() {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-filter-group">
|
|
||||||
<label className="admin-filter-label">Objednávka</label>
|
|
||||||
<select
|
|
||||||
value={orderFilter}
|
|
||||||
onChange={(e) => {
|
|
||||||
setOrderFilter(e.target.value);
|
|
||||||
setPage(1);
|
|
||||||
}}
|
|
||||||
className="admin-form-select"
|
|
||||||
>
|
|
||||||
{ORDER_FILTERS.map((f) => (
|
|
||||||
<option key={f.value} value={f.value}>
|
|
||||||
{f.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
<motion.div
|
<motion.div
|
||||||
key={`${statusFilter}-${customerFilter}-${orderFilter}-${search}-${page}-${quotations.length}`}
|
key={`${statusFilter}-${customerFilter}-${search}-${page}-${quotations.length}`}
|
||||||
initial={{ opacity: 0, y: 6 }}
|
initial={{ opacity: 0, y: 6 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
@@ -959,40 +933,20 @@ export default function Offers() {
|
|||||||
loading={invalidating}
|
loading={invalidating}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AnimatePresence>
|
<FormModal
|
||||||
{orderModal.show && (
|
isOpen={orderModal.show}
|
||||||
<motion.div
|
onClose={() => setOrderModal({ show: false, quotation: null })}
|
||||||
className="admin-modal-overlay"
|
onSubmit={handleCreateOrder}
|
||||||
initial={{ opacity: 0 }}
|
title="Vytvořit objednávku"
|
||||||
animate={{ opacity: 1 }}
|
submitLabel={creatingOrder ? "Vytváření..." : "Vytvořit"}
|
||||||
exit={{ opacity: 0 }}
|
loading={!!creatingOrder}
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
className="admin-modal-backdrop"
|
|
||||||
onClick={() =>
|
|
||||||
!creatingOrder &&
|
|
||||||
setOrderModal({ show: false, quotation: null })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<motion.div
|
|
||||||
className="admin-modal"
|
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
|
||||||
<div className="admin-modal-header">
|
|
||||||
<h2 className="admin-modal-title">Vytvořit objednávku</h2>
|
|
||||||
<p
|
<p
|
||||||
className="text-secondary text-md"
|
className="text-secondary text-md"
|
||||||
style={{ marginTop: "0.25rem" }}
|
style={{ marginTop: "-0.5rem", marginBottom: "0.5rem" }}
|
||||||
>
|
>
|
||||||
Nabídka:{" "}
|
Nabídka: <strong>{orderModal.quotation?.quotation_number}</strong>
|
||||||
<strong>{orderModal.quotation?.quotation_number}</strong>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
<div className="admin-modal-body">
|
|
||||||
<div className="admin-form">
|
<div className="admin-form">
|
||||||
<FormField label="Číslo objednávky zákazníka" required>
|
<FormField label="Číslo objednávky zákazníka" required>
|
||||||
<input
|
<input
|
||||||
@@ -1000,9 +954,7 @@ export default function Offers() {
|
|||||||
value={customerOrderNumber}
|
value={customerOrderNumber}
|
||||||
onChange={(e) => setCustomerOrderNumber(e.target.value)}
|
onChange={(e) => setCustomerOrderNumber(e.target.value)}
|
||||||
onKeyDown={(e) =>
|
onKeyDown={(e) =>
|
||||||
e.key === "Enter" &&
|
e.key === "Enter" && !creatingOrder && handleCreateOrder()
|
||||||
!creatingOrder &&
|
|
||||||
handleCreateOrder()
|
|
||||||
}
|
}
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
placeholder="Např. PO-2026-001"
|
placeholder="Např. PO-2026-001"
|
||||||
@@ -1079,37 +1031,12 @@ export default function Offers() {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
<small
|
<small className="admin-form-hint" style={{ marginTop: "0.25rem" }}>
|
||||||
className="admin-form-hint"
|
|
||||||
style={{ marginTop: "0.25rem" }}
|
|
||||||
>
|
|
||||||
Max 10 MB
|
Max 10 MB
|
||||||
</small>
|
</small>
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</FormModal>
|
||||||
<div className="admin-modal-footer">
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
setOrderModal({ show: false, quotation: null })
|
|
||||||
}
|
|
||||||
className="admin-btn admin-btn-secondary"
|
|
||||||
disabled={!!creatingOrder}
|
|
||||||
>
|
|
||||||
Zrušit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleCreateOrder}
|
|
||||||
className="admin-btn admin-btn-primary"
|
|
||||||
disabled={!!creatingOrder || !customerOrderNumber.trim()}
|
|
||||||
>
|
|
||||||
{creatingOrder ? "Vytváření..." : "Vytvořit"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,18 @@
|
|||||||
import { useState, useCallback, useRef } from "react";
|
import { useState, useCallback, useRef } from "react";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
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 { motion, AnimatePresence } 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 FormField from "../components/FormField";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import useModalLock from "../hooks/useModalLock";
|
|
||||||
import {
|
import {
|
||||||
offerCustomersOptions,
|
offerCustomersOptions,
|
||||||
type Customer,
|
type Customer,
|
||||||
type CustomField,
|
type CustomField,
|
||||||
} from "../lib/queries/offers";
|
} from "../lib/queries/offers";
|
||||||
import { Skeleton } from "boneyard-js/react";
|
import { useApiMutation } from "../lib/queries/mutations";
|
||||||
import OffersCustomersFixture from "../fixtures/OffersCustomersFixture";
|
|
||||||
|
|
||||||
import apiFetch from "../utils/api";
|
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
|
|
||||||
@@ -48,7 +45,6 @@ interface CustomerForm {
|
|||||||
export default function OffersCustomers() {
|
export default function OffersCustomers() {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const { data: customers = [], isPending } = useQuery(offerCustomersOptions());
|
const { data: customers = [], isPending } = useQuery(offerCustomersOptions());
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
@@ -76,7 +72,44 @@ export default function OffersCustomers() {
|
|||||||
}>({ show: false, customer: null });
|
}>({ show: false, customer: null });
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
|
||||||
useModalLock(showModal);
|
const submitMutation = useApiMutation<
|
||||||
|
CustomerForm & {
|
||||||
|
custom_fields: Array<{
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
showLabel?: boolean;
|
||||||
|
}>;
|
||||||
|
customer_field_order: string[];
|
||||||
|
},
|
||||||
|
{ message?: string; error?: string }
|
||||||
|
>({
|
||||||
|
url: () =>
|
||||||
|
editingCustomer
|
||||||
|
? `${API_BASE}/customers/${editingCustomer.id}`
|
||||||
|
: `${API_BASE}/customers`,
|
||||||
|
method: () => (editingCustomer ? "PUT" : "POST"),
|
||||||
|
invalidate: ["offer-customers", "offers"],
|
||||||
|
onSuccess: (data) => {
|
||||||
|
closeModal();
|
||||||
|
setTimeout(
|
||||||
|
() => alert.success(data?.message || "Zákazník byl uložen"),
|
||||||
|
300,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteMutation = useApiMutation<
|
||||||
|
number,
|
||||||
|
{ message?: string; error?: string }
|
||||||
|
>({
|
||||||
|
url: (id) => `${API_BASE}/customers/${id}`,
|
||||||
|
method: () => "DELETE",
|
||||||
|
invalidate: ["offer-customers", "offers"],
|
||||||
|
onSuccess: (data) => {
|
||||||
|
setDeleteConfirm({ show: false, customer: null });
|
||||||
|
alert.success(data?.message || "Zákazník byl smazán");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const getFullFieldOrder = useCallback(() => {
|
const getFullFieldOrder = useCallback(() => {
|
||||||
const allBuiltIn = [...DEFAULT_CUSTOMER_FIELD_ORDER];
|
const allBuiltIn = [...DEFAULT_CUSTOMER_FIELD_ORDER];
|
||||||
@@ -122,8 +155,6 @@ export default function OffersCustomers() {
|
|||||||
return key;
|
return key;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Data fetching moved to useQuery(offerCustomersOptions()) above
|
|
||||||
|
|
||||||
const openCreateModal = () => {
|
const openCreateModal = () => {
|
||||||
setEditingCustomer(null);
|
setEditingCustomer(null);
|
||||||
setForm({
|
setForm({
|
||||||
@@ -181,44 +212,23 @@ export default function OffersCustomers() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSaving(true);
|
|
||||||
try {
|
|
||||||
const url = editingCustomer
|
|
||||||
? `${API_BASE}/customers/${editingCustomer.id}`
|
|
||||||
: `${API_BASE}/customers`;
|
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
...form,
|
...form,
|
||||||
custom_fields: customFields.filter(
|
custom_fields: customFields
|
||||||
(f) => f.name.trim() || f.value.trim(),
|
.filter((f) => f.name.trim() || f.value.trim())
|
||||||
),
|
.map((f) => ({
|
||||||
|
name: f.name,
|
||||||
|
value: f.value,
|
||||||
|
showLabel: f.showLabel,
|
||||||
|
})),
|
||||||
customer_field_order: getFullFieldOrder(),
|
customer_field_order: getFullFieldOrder(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await apiFetch(url, {
|
setSaving(true);
|
||||||
method: editingCustomer ? "PUT" : "POST",
|
try {
|
||||||
headers: { "Content-Type": "application/json" },
|
await submitMutation.mutateAsync(payload);
|
||||||
body: JSON.stringify(payload),
|
} catch (e) {
|
||||||
});
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
closeModal();
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
||||||
alert.success(
|
|
||||||
result.message ||
|
|
||||||
(editingCustomer
|
|
||||||
? "Zákazník byl aktualizován"
|
|
||||||
: "Zákazník byl vytvořen"),
|
|
||||||
);
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["offer-customers"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
|
||||||
} else {
|
|
||||||
alert.error(result.error || "Nepodařilo se uložit zákazníka");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -229,25 +239,9 @@ export default function OffersCustomers() {
|
|||||||
|
|
||||||
setDeleting(true);
|
setDeleting(true);
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(
|
await deleteMutation.mutateAsync(deleteConfirm.customer.id);
|
||||||
`${API_BASE}/customers/${deleteConfirm.customer.id}`,
|
} catch (e) {
|
||||||
{
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
method: "DELETE",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
setDeleteConfirm({ show: false, customer: null });
|
|
||||||
alert.success(result.message || "Zákazník byl smazán");
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["offer-customers"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
|
||||||
} else {
|
|
||||||
alert.error(result.error || "Nepodařilo se smazat zákazníka");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
} finally {
|
||||||
setDeleting(false);
|
setDeleting(false);
|
||||||
}
|
}
|
||||||
@@ -266,12 +260,14 @@ export default function OffersCustomers() {
|
|||||||
|
|
||||||
const fullFieldOrder = getFullFieldOrder();
|
const fullFieldOrder = getFullFieldOrder();
|
||||||
|
|
||||||
|
if (isPending) {
|
||||||
|
return (
|
||||||
|
<div className="admin-loading">
|
||||||
|
<div className="admin-spinner" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Skeleton
|
|
||||||
name="offers-customers"
|
|
||||||
loading={isPending}
|
|
||||||
fixture={<OffersCustomersFixture />}
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="admin-page-header"
|
className="admin-page-header"
|
||||||
@@ -444,31 +440,15 @@ export default function OffersCustomers() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Create/Edit Modal */}
|
{/* Create/Edit Modal */}
|
||||||
<AnimatePresence>
|
<FormModal
|
||||||
{showModal && (
|
isOpen={showModal}
|
||||||
<motion.div
|
onClose={closeModal}
|
||||||
className="admin-modal-overlay"
|
onSubmit={handleSubmit}
|
||||||
initial={{ opacity: 0 }}
|
title={editingCustomer ? "Upravit zákazníka" : "Nový zákazník"}
|
||||||
animate={{ opacity: 1 }}
|
submitLabel={editingCustomer ? "Uložit změny" : "Vytvořit zákazníka"}
|
||||||
exit={{ opacity: 0 }}
|
loading={saving}
|
||||||
transition={{ duration: 0.2 }}
|
size="lg"
|
||||||
>
|
>
|
||||||
<div className="admin-modal-backdrop" onClick={closeModal} />
|
|
||||||
<motion.div
|
|
||||||
className="admin-modal"
|
|
||||||
style={{ maxWidth: 720 }}
|
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
|
||||||
<div className="admin-modal-header">
|
|
||||||
<h2 className="admin-modal-title">
|
|
||||||
{editingCustomer ? "Upravit zákazníka" : "Nový zákazník"}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="admin-modal-body">
|
|
||||||
<div className="admin-form">
|
<div className="admin-form">
|
||||||
<FormField label="Název" required>
|
<FormField label="Název" required>
|
||||||
<input
|
<input
|
||||||
@@ -486,10 +466,7 @@ export default function OffersCustomers() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={form.street}
|
value={form.street}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setForm((prev) => ({
|
setForm((prev) => ({ ...prev, street: e.target.value }))
|
||||||
...prev,
|
|
||||||
street: e.target.value,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
/>
|
/>
|
||||||
@@ -500,10 +477,7 @@ export default function OffersCustomers() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={form.city}
|
value={form.city}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setForm((prev) => ({
|
setForm((prev) => ({ ...prev, city: e.target.value }))
|
||||||
...prev,
|
|
||||||
city: e.target.value,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
/>
|
/>
|
||||||
@@ -527,10 +501,7 @@ export default function OffersCustomers() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={form.country}
|
value={form.country}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setForm((prev) => ({
|
setForm((prev) => ({ ...prev, country: e.target.value }))
|
||||||
...prev,
|
|
||||||
country: e.target.value,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
/>
|
/>
|
||||||
@@ -554,10 +525,7 @@ export default function OffersCustomers() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={form.vat_id}
|
value={form.vat_id}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setForm((prev) => ({
|
setForm((prev) => ({ ...prev, vat_id: e.target.value }))
|
||||||
...prev,
|
|
||||||
vat_id: e.target.value,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
/>
|
/>
|
||||||
@@ -579,7 +547,7 @@ export default function OffersCustomers() {
|
|||||||
style={{ marginBottom: 0, alignItems: "flex-end" }}
|
style={{ marginBottom: 0, alignItems: "flex-end" }}
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
label={idx === 0 ? "Název" : "\u00A0"}
|
label={idx === 0 ? "Název" : " "}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -598,7 +566,7 @@ export default function OffersCustomers() {
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField
|
<FormField
|
||||||
label={idx === 0 ? "Hodnota" : "\u00A0"}
|
label={idx === 0 ? "Hodnota" : " "}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -631,11 +599,8 @@ export default function OffersCustomers() {
|
|||||||
.filter((k) => k !== key)
|
.filter((k) => k !== key)
|
||||||
.map((k) => {
|
.map((k) => {
|
||||||
if (k.startsWith("custom_")) {
|
if (k.startsWith("custom_")) {
|
||||||
const ki = parseInt(
|
const ki = parseInt(k.split("_")[1]);
|
||||||
k.split("_")[1],
|
if (ki > idx) return `custom_${ki - 1}`;
|
||||||
);
|
|
||||||
if (ki > idx)
|
|
||||||
return `custom_${ki - 1}`;
|
|
||||||
}
|
}
|
||||||
return k;
|
return k;
|
||||||
});
|
});
|
||||||
@@ -663,10 +628,7 @@ export default function OffersCustomers() {
|
|||||||
</div>
|
</div>
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
<label
|
<label className="admin-form-checkbox" style={{ marginTop: 4 }}>
|
||||||
className="admin-form-checkbox"
|
|
||||||
style={{ marginTop: 4 }}
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={field.showLabel !== false}
|
checked={field.showLabel !== false}
|
||||||
@@ -718,15 +680,12 @@ export default function OffersCustomers() {
|
|||||||
|
|
||||||
{/* Field order for PDF */}
|
{/* Field order for PDF */}
|
||||||
<div style={{ marginTop: 16 }}>
|
<div style={{ marginTop: 16 }}>
|
||||||
<label className="admin-form-label">
|
<label className="admin-form-label">Pořadí polí v PDF</label>
|
||||||
Pořadí polí v PDF
|
|
||||||
</label>
|
|
||||||
<small
|
<small
|
||||||
className="admin-form-hint"
|
className="admin-form-hint"
|
||||||
style={{ display: "block", marginBottom: 8 }}
|
style={{ display: "block", marginBottom: 8 }}
|
||||||
>
|
>
|
||||||
Určuje pořadí řádků v adresním bloku zákazníka na PDF
|
Určuje pořadí řádků v adresním bloku zákazníka na PDF nabídce.
|
||||||
nabídce.
|
|
||||||
</small>
|
</small>
|
||||||
<div className="admin-reorder-list">
|
<div className="admin-reorder-list">
|
||||||
{fullFieldOrder.map((key, index) => (
|
{fullFieldOrder.map((key, index) => (
|
||||||
@@ -781,37 +740,7 @@ export default function OffersCustomers() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</FormModal>
|
||||||
|
|
||||||
<div className="admin-modal-footer">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={closeModal}
|
|
||||||
className="admin-btn admin-btn-secondary"
|
|
||||||
disabled={saving}
|
|
||||||
>
|
|
||||||
Zrušit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleSubmit}
|
|
||||||
className="admin-btn admin-btn-primary"
|
|
||||||
disabled={saving}
|
|
||||||
>
|
|
||||||
{saving && (
|
|
||||||
<>
|
|
||||||
<div className="admin-spinner admin-spinner-sm" />
|
|
||||||
Ukládání...
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!saving &&
|
|
||||||
(editingCustomer ? "Uložit změny" : "Vytvořit zákazníka")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
{/* Delete Confirm Modal */}
|
{/* Delete Confirm Modal */}
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
@@ -826,6 +755,5 @@ export default function OffersCustomers() {
|
|||||||
loading={deleting}
|
loading={deleting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Skeleton>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useState, useRef, type ReactNode } from "react";
|
import { useState, useRef, type ReactNode } from "react";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
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 { motion, AnimatePresence } 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 FormField from "../components/FormField";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import RichEditor from "../components/RichEditor";
|
import RichEditor from "../components/RichEditor";
|
||||||
import useModalLock from "../hooks/useModalLock";
|
|
||||||
import {
|
import {
|
||||||
itemTemplatesOptions,
|
itemTemplatesOptions,
|
||||||
scopeTemplatesOptions,
|
scopeTemplatesOptions,
|
||||||
@@ -15,8 +15,7 @@ import {
|
|||||||
type ScopeTemplate,
|
type ScopeTemplate,
|
||||||
type ScopeSection,
|
type ScopeSection,
|
||||||
} from "../lib/queries/offers";
|
} from "../lib/queries/offers";
|
||||||
import { Skeleton } from "boneyard-js/react";
|
import { useApiMutation } from "../lib/queries/mutations";
|
||||||
import OffersTemplatesFixture from "../fixtures/OffersTemplatesFixture";
|
|
||||||
|
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
|
|
||||||
@@ -80,7 +79,6 @@ export default function OffersTemplates() {
|
|||||||
|
|
||||||
function ItemTemplatesTab() {
|
function ItemTemplatesTab() {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const { data: templates = [], isPending } = useQuery(itemTemplatesOptions());
|
const { data: templates = [], isPending } = useQuery(itemTemplatesOptions());
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [editingTemplate, setEditingTemplate] = useState<ItemTemplate | null>(
|
const [editingTemplate, setEditingTemplate] = useState<ItemTemplate | null>(
|
||||||
@@ -99,7 +97,31 @@ function ItemTemplatesTab() {
|
|||||||
}>({ show: false, template: null });
|
}>({ show: false, template: null });
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
|
||||||
useModalLock(showModal);
|
const submitMutation = useApiMutation<
|
||||||
|
ItemForm & { id?: number },
|
||||||
|
{ message?: string; error?: string }
|
||||||
|
>({
|
||||||
|
url: () => `${API_BASE}/offers-templates?action=item`,
|
||||||
|
method: () => "POST",
|
||||||
|
invalidate: ["offer-templates", "offers"],
|
||||||
|
onSuccess: (data) => {
|
||||||
|
setShowModal(false);
|
||||||
|
setTimeout(() => alert.success(data?.message || "Uloženo"), 300);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteMutation = useApiMutation<
|
||||||
|
number,
|
||||||
|
{ message?: string; error?: string }
|
||||||
|
>({
|
||||||
|
url: (id) => `${API_BASE}/offers-templates?action=item&id=${id}`,
|
||||||
|
method: () => "DELETE",
|
||||||
|
invalidate: ["offer-templates", "offers"],
|
||||||
|
onSuccess: (data) => {
|
||||||
|
setDeleteConfirm({ show: false, template: null });
|
||||||
|
alert.success(data?.message || "Smazáno");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
setEditingTemplate(null);
|
setEditingTemplate(null);
|
||||||
@@ -123,29 +145,12 @@ function ItemTemplatesTab() {
|
|||||||
alert.error("Název šablony je povinný");
|
alert.error("Název šablony je povinný");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const body = editingTemplate ? { ...form, id: editingTemplate.id } : form;
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const body = editingTemplate ? { ...form, id: editingTemplate.id } : form;
|
await submitMutation.mutateAsync(body);
|
||||||
const response = await apiFetch(
|
} catch (e) {
|
||||||
`${API_BASE}/offers-templates?action=item`,
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setShowModal(false);
|
|
||||||
await new Promise((r) => setTimeout(r, 300));
|
|
||||||
alert.success(result.message);
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["offer-templates"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
|
||||||
} else {
|
|
||||||
alert.error(result.error);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -155,32 +160,22 @@ function ItemTemplatesTab() {
|
|||||||
if (!deleteConfirm.template) return;
|
if (!deleteConfirm.template) return;
|
||||||
setDeleting(true);
|
setDeleting(true);
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(
|
await deleteMutation.mutateAsync(deleteConfirm.template.id);
|
||||||
`${API_BASE}/offers-templates?action=item&id=${deleteConfirm.template.id}`,
|
} catch (e) {
|
||||||
{ method: "DELETE" },
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
);
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setDeleteConfirm({ show: false, template: null });
|
|
||||||
alert.success(result.message);
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["offer-templates"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
|
||||||
} else {
|
|
||||||
alert.error(result.error);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
} finally {
|
||||||
setDeleting(false);
|
setDeleting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isPending) {
|
||||||
|
return (
|
||||||
|
<div className="admin-loading">
|
||||||
|
<div className="admin-spinner" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Skeleton
|
|
||||||
name="offers-templates"
|
|
||||||
loading={isPending}
|
|
||||||
fixture={<OffersTemplatesFixture />}
|
|
||||||
>
|
|
||||||
<>
|
<>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="admin-card"
|
className="admin-card"
|
||||||
@@ -290,42 +285,20 @@ function ItemTemplatesTab() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Item Template Modal */}
|
{/* Item Template Modal */}
|
||||||
<AnimatePresence>
|
<FormModal
|
||||||
{showModal && (
|
isOpen={showModal}
|
||||||
<motion.div
|
onClose={() => setShowModal(false)}
|
||||||
className="admin-modal-overlay"
|
onSubmit={handleSubmit}
|
||||||
initial={{ opacity: 0 }}
|
title={editingTemplate ? "Upravit šablonu" : "Nová šablona položky"}
|
||||||
animate={{ opacity: 1 }}
|
submitLabel={editingTemplate ? "Uložit" : "Vytvořit"}
|
||||||
exit={{ opacity: 0 }}
|
loading={saving}
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
className="admin-modal-backdrop"
|
|
||||||
onClick={() => setShowModal(false)}
|
|
||||||
/>
|
|
||||||
<motion.div
|
|
||||||
className="admin-modal"
|
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
|
||||||
<div className="admin-modal-header">
|
|
||||||
<h2 className="admin-modal-title">
|
|
||||||
{editingTemplate
|
|
||||||
? "Upravit šablonu"
|
|
||||||
: "Nová šablona položky"}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div className="admin-modal-body">
|
|
||||||
<div className="admin-form">
|
<div className="admin-form">
|
||||||
<FormField label="Název" required>
|
<FormField label="Název" required>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={form.name}
|
value={form.name}
|
||||||
onChange={(e) =>
|
onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))}
|
||||||
setForm((p) => ({ ...p, name: e.target.value }))
|
|
||||||
}
|
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
@@ -355,6 +328,7 @@ function ItemTemplatesTab() {
|
|||||||
}
|
}
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
|
inputMode="decimal"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Kategorie">
|
<FormField label="Kategorie">
|
||||||
@@ -369,35 +343,7 @@ function ItemTemplatesTab() {
|
|||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</FormModal>
|
||||||
<div className="admin-modal-footer">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowModal(false)}
|
|
||||||
className="admin-btn admin-btn-secondary"
|
|
||||||
disabled={saving}
|
|
||||||
>
|
|
||||||
Zrušit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleSubmit}
|
|
||||||
className="admin-btn admin-btn-primary"
|
|
||||||
disabled={saving}
|
|
||||||
>
|
|
||||||
{saving && (
|
|
||||||
<>
|
|
||||||
<div className="admin-spinner admin-spinner-sm" />
|
|
||||||
Ukládání...
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!saving && (editingTemplate ? "Uložit" : "Vytvořit")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={deleteConfirm.show}
|
isOpen={deleteConfirm.show}
|
||||||
@@ -411,7 +357,6 @@ function ItemTemplatesTab() {
|
|||||||
loading={deleting}
|
loading={deleting}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
</Skeleton>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,7 +364,6 @@ function ItemTemplatesTab() {
|
|||||||
|
|
||||||
function ScopeTemplatesTab() {
|
function ScopeTemplatesTab() {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const { data: templates = [], isPending } = useQuery(scopeTemplatesOptions());
|
const { data: templates = [], isPending } = useQuery(scopeTemplatesOptions());
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [editingTemplate, setEditingTemplate] = useState<ScopeTemplate | null>(
|
const [editingTemplate, setEditingTemplate] = useState<ScopeTemplate | null>(
|
||||||
@@ -434,7 +378,34 @@ function ScopeTemplatesTab() {
|
|||||||
}>({ show: false, template: null });
|
}>({ show: false, template: null });
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
|
||||||
useModalLock(showModal);
|
const submitMutation = useApiMutation<
|
||||||
|
ScopeForm,
|
||||||
|
{ message?: string; error?: string }
|
||||||
|
>({
|
||||||
|
url: () =>
|
||||||
|
editingTemplate
|
||||||
|
? `${API_BASE}/offers-templates/${editingTemplate.id}`
|
||||||
|
: `${API_BASE}/offers-templates`,
|
||||||
|
method: () => (editingTemplate ? "PUT" : "POST"),
|
||||||
|
invalidate: ["offer-templates", "offers"],
|
||||||
|
onSuccess: (data) => {
|
||||||
|
setShowModal(false);
|
||||||
|
setTimeout(() => alert.success(data?.message || "Uloženo"), 300);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteMutation = useApiMutation<
|
||||||
|
number,
|
||||||
|
{ message?: string; error?: string }
|
||||||
|
>({
|
||||||
|
url: (id) => `${API_BASE}/offers-templates/${id}`,
|
||||||
|
method: () => "DELETE",
|
||||||
|
invalidate: ["offer-templates", "offers"],
|
||||||
|
onSuccess: (data) => {
|
||||||
|
setDeleteConfirm({ show: false, template: null });
|
||||||
|
alert.success(data?.message || "Smazáno");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
setEditingTemplate(null);
|
setEditingTemplate(null);
|
||||||
@@ -542,27 +513,9 @@ function ScopeTemplatesTab() {
|
|||||||
}
|
}
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const url = editingTemplate
|
await submitMutation.mutateAsync(form);
|
||||||
? `${API_BASE}/offers-templates/${editingTemplate.id}`
|
} catch (e) {
|
||||||
: `${API_BASE}/offers-templates`;
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
const method = editingTemplate ? "PUT" : "POST";
|
|
||||||
const response = await apiFetch(url, {
|
|
||||||
method,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(form),
|
|
||||||
});
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setShowModal(false);
|
|
||||||
await new Promise((r) => setTimeout(r, 300));
|
|
||||||
alert.success(result.message);
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["offer-templates"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
|
||||||
} else {
|
|
||||||
alert.error(result.error);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -572,32 +525,15 @@ function ScopeTemplatesTab() {
|
|||||||
if (!deleteConfirm.template) return;
|
if (!deleteConfirm.template) return;
|
||||||
setDeleting(true);
|
setDeleting(true);
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(
|
await deleteMutation.mutateAsync(deleteConfirm.template.id);
|
||||||
`${API_BASE}/offers-templates/${deleteConfirm.template.id}`,
|
} catch (e) {
|
||||||
{ method: "DELETE" },
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
);
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setDeleteConfirm({ show: false, template: null });
|
|
||||||
alert.success(result.message);
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["offer-templates"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
|
||||||
} else {
|
|
||||||
alert.error(result.error);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
} finally {
|
||||||
setDeleting(false);
|
setDeleting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Skeleton
|
|
||||||
name="offers-templates"
|
|
||||||
loading={isPending}
|
|
||||||
fixture={<OffersTemplatesFixture />}
|
|
||||||
>
|
|
||||||
<>
|
<>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="admin-card"
|
className="admin-card"
|
||||||
@@ -697,42 +633,23 @@ function ScopeTemplatesTab() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Scope Template Modal (large) */}
|
{/* Scope Template Modal (large) */}
|
||||||
<AnimatePresence>
|
<FormModal
|
||||||
{showModal && (
|
isOpen={showModal}
|
||||||
<motion.div
|
onClose={() => setShowModal(false)}
|
||||||
className="admin-modal-overlay"
|
onSubmit={handleSubmit}
|
||||||
initial={{ opacity: 0 }}
|
title={
|
||||||
animate={{ opacity: 1 }}
|
editingTemplate ? "Upravit šablonu rozsahu" : "Nová šablona rozsahu"
|
||||||
exit={{ opacity: 0 }}
|
}
|
||||||
transition={{ duration: 0.2 }}
|
submitLabel={editingTemplate ? "Uložit" : "Vytvořit"}
|
||||||
|
size="lg"
|
||||||
|
loading={saving}
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
className="admin-modal-backdrop"
|
|
||||||
onClick={() => setShowModal(false)}
|
|
||||||
/>
|
|
||||||
<motion.div
|
|
||||||
className="admin-modal admin-modal-lg"
|
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
|
||||||
<div className="admin-modal-header">
|
|
||||||
<h2 className="admin-modal-title">
|
|
||||||
{editingTemplate
|
|
||||||
? "Upravit šablonu rozsahu"
|
|
||||||
: "Nová šablona rozsahu"}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div className="admin-modal-body">
|
|
||||||
<div className="admin-form">
|
<div className="admin-form">
|
||||||
<FormField label="Název šablony" required>
|
<FormField label="Název šablony" required>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={form.name}
|
value={form.name}
|
||||||
onChange={(e) =>
|
onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))}
|
||||||
setForm((p) => ({ ...p, name: e.target.value }))
|
|
||||||
}
|
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
@@ -741,14 +658,9 @@ function ScopeTemplatesTab() {
|
|||||||
<label className="admin-form-label mb-2">Sekce</label>
|
<label className="admin-form-label mb-2">Sekce</label>
|
||||||
<div className="admin-scope-list">
|
<div className="admin-scope-list">
|
||||||
{form.sections.map((section, index) => (
|
{form.sections.map((section, index) => (
|
||||||
<div
|
<div key={section._key} className="admin-scope-section">
|
||||||
key={section._key}
|
|
||||||
className="admin-scope-section"
|
|
||||||
>
|
|
||||||
<div className="admin-scope-section-header">
|
<div className="admin-scope-section-header">
|
||||||
<span className="admin-scope-number">
|
<span className="admin-scope-number">{index + 1}.</span>
|
||||||
{index + 1}.
|
|
||||||
</span>
|
|
||||||
<span className="admin-scope-title">
|
<span className="admin-scope-title">
|
||||||
{section.title ||
|
{section.title ||
|
||||||
section.title_cz ||
|
section.title_cz ||
|
||||||
@@ -821,10 +733,8 @@ function ScopeTemplatesTab() {
|
|||||||
<FormField
|
<FormField
|
||||||
label={
|
label={
|
||||||
<>
|
<>
|
||||||
<span className="offers-lang-badge">
|
<span className="offers-lang-badge">EN</span> Název
|
||||||
EN
|
sekce
|
||||||
</span>{" "}
|
|
||||||
Název sekce
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -832,11 +742,7 @@ function ScopeTemplatesTab() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={section.title}
|
value={section.title}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateSection(
|
updateSection(index, "title", e.target.value)
|
||||||
index,
|
|
||||||
"title",
|
|
||||||
e.target.value,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
placeholder="Název sekce (anglicky)"
|
placeholder="Název sekce (anglicky)"
|
||||||
@@ -856,11 +762,7 @@ function ScopeTemplatesTab() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={section.title_cz}
|
value={section.title_cz}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateSection(
|
updateSection(index, "title_cz", e.target.value)
|
||||||
index,
|
|
||||||
"title_cz",
|
|
||||||
e.target.value,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
className="admin-form-input"
|
className="admin-form-input"
|
||||||
placeholder="Název sekce (česky)"
|
placeholder="Název sekce (česky)"
|
||||||
@@ -870,9 +772,7 @@ function ScopeTemplatesTab() {
|
|||||||
<FormField label="Obsah">
|
<FormField label="Obsah">
|
||||||
<RichEditor
|
<RichEditor
|
||||||
value={section.content}
|
value={section.content}
|
||||||
onChange={(val) =>
|
onChange={(val) => updateSection(index, "content", val)}
|
||||||
updateSection(index, "content", val)
|
|
||||||
}
|
|
||||||
placeholder="Obsah sekce..."
|
placeholder="Obsah sekce..."
|
||||||
minHeight="150px"
|
minHeight="150px"
|
||||||
/>
|
/>
|
||||||
@@ -892,35 +792,7 @@ function ScopeTemplatesTab() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</FormModal>
|
||||||
<div className="admin-modal-footer">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowModal(false)}
|
|
||||||
className="admin-btn admin-btn-secondary"
|
|
||||||
disabled={saving}
|
|
||||||
>
|
|
||||||
Zrušit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleSubmit}
|
|
||||||
className="admin-btn admin-btn-primary"
|
|
||||||
disabled={saving}
|
|
||||||
>
|
|
||||||
{saving && (
|
|
||||||
<>
|
|
||||||
<div className="admin-spinner admin-spinner-sm" />
|
|
||||||
Ukládání...
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!saving && (editingTemplate ? "Uložit" : "Vytvořit")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={deleteConfirm.show}
|
isOpen={deleteConfirm.show}
|
||||||
@@ -934,6 +806,5 @@ function ScopeTemplatesTab() {
|
|||||||
loading={deleting}
|
loading={deleting}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
</Skeleton>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useMemo, useRef, type ReactNode } from "react";
|
import { useState, useEffect, useMemo, useRef, type ReactNode } from "react";
|
||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
orderDetailOptions,
|
orderDetailOptions,
|
||||||
type OrderData,
|
type OrderData,
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
type OrderInvoice,
|
type OrderInvoice,
|
||||||
type OrderProject,
|
type OrderProject,
|
||||||
} from "../lib/queries/orders";
|
} from "../lib/queries/orders";
|
||||||
|
import { useApiMutation } from "../lib/queries/mutations";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { useParams, useNavigate, Link } from "react-router-dom";
|
import { useParams, useNavigate, Link } from "react-router-dom";
|
||||||
@@ -17,9 +18,6 @@ import ConfirmModal from "../components/ConfirmModal";
|
|||||||
import OrderConfirmationModal from "../components/OrderConfirmationModal";
|
import OrderConfirmationModal from "../components/OrderConfirmationModal";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import { Skeleton } from "boneyard-js/react";
|
|
||||||
import OrderDetailFixture from "../fixtures/OrderDetailFixture";
|
|
||||||
|
|
||||||
import apiFetch from "../utils/api";
|
import apiFetch from "../utils/api";
|
||||||
import { formatCurrency, formatDate } from "../utils/formatters";
|
import { formatCurrency, formatDate } from "../utils/formatters";
|
||||||
|
|
||||||
@@ -55,7 +53,6 @@ export default function OrderDetail() {
|
|||||||
const { hasPermission } = useAuth();
|
const { hasPermission } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const orderQuery = useQuery(orderDetailOptions(id));
|
const orderQuery = useQuery(orderDetailOptions(id));
|
||||||
const order = orderQuery.data;
|
const order = orderQuery.data;
|
||||||
const loading = orderQuery.isPending;
|
const loading = orderQuery.isPending;
|
||||||
@@ -139,28 +136,37 @@ export default function OrderDetail() {
|
|||||||
|
|
||||||
if (!hasPermission("orders.view")) return <Forbidden />;
|
if (!hasPermission("orders.view")) return <Forbidden />;
|
||||||
|
|
||||||
|
const statusMutation = useApiMutation<{ status: string }, unknown>({
|
||||||
|
url: () => `${API_BASE}/orders/${id}`,
|
||||||
|
method: () => "PUT",
|
||||||
|
invalidate: ["orders", "invoices"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const notesMutation = useApiMutation<{ notes: string }, unknown>({
|
||||||
|
url: () => `${API_BASE}/orders/${id}`,
|
||||||
|
method: () => "PUT",
|
||||||
|
invalidate: ["orders", "invoices"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const orderDeleteMutation = useApiMutation<
|
||||||
|
{ delete_files: boolean },
|
||||||
|
unknown
|
||||||
|
>({
|
||||||
|
url: () => `${API_BASE}/orders/${id}`,
|
||||||
|
method: () => "DELETE",
|
||||||
|
invalidate: ["orders", "invoices"],
|
||||||
|
});
|
||||||
|
|
||||||
const handleStatusChange = async () => {
|
const handleStatusChange = async () => {
|
||||||
if (!statusConfirm.status) return;
|
if (!statusConfirm.status) return;
|
||||||
setStatusChanging(statusConfirm.status);
|
const newStatus = statusConfirm.status;
|
||||||
|
setStatusChanging(newStatus);
|
||||||
setStatusConfirm({ show: false, status: null });
|
setStatusConfirm({ show: false, status: null });
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(`${API_BASE}/orders/${id}`, {
|
await statusMutation.mutateAsync({ status: newStatus });
|
||||||
method: "PUT",
|
alert.success("Stav byl změněn");
|
||||||
headers: { "Content-Type": "application/json" },
|
} catch (e) {
|
||||||
body: JSON.stringify({ status: statusConfirm.status }),
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
});
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
alert.success(result.message || "Stav byl změněn");
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
|
||||||
} else {
|
|
||||||
alert.error(result.error || "Nepodařilo se změnit stav");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
} finally {
|
||||||
setStatusChanging(null);
|
setStatusChanging(null);
|
||||||
}
|
}
|
||||||
@@ -169,24 +175,11 @@ export default function OrderDetail() {
|
|||||||
const handleSaveNotes = async () => {
|
const handleSaveNotes = async () => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(`${API_BASE}/orders/${id}`, {
|
await notesMutation.mutateAsync({ notes });
|
||||||
method: "PUT",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ notes: notes }),
|
|
||||||
});
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
alert.success("Poznámky byly uloženy");
|
alert.success("Poznámky byly uloženy");
|
||||||
initialNotesRef.current = notes;
|
initialNotesRef.current = notes;
|
||||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
} catch (e) {
|
||||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
|
||||||
} else {
|
|
||||||
alert.error(result.error || "Nepodařilo se uložit poznámky");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -262,24 +255,11 @@ export default function OrderDetail() {
|
|||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
setDeleting(true);
|
setDeleting(true);
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(`${API_BASE}/orders/${id}`, {
|
await orderDeleteMutation.mutateAsync({ delete_files: deleteFiles });
|
||||||
method: "DELETE",
|
alert.success("Objednávka byla smazána");
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ delete_files: deleteFiles }),
|
|
||||||
});
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
alert.success(result.message || "Objednávka byla smazána");
|
|
||||||
await queryClient.invalidateQueries({ queryKey: ["orders"] });
|
|
||||||
await queryClient.invalidateQueries({ queryKey: ["offers"] });
|
|
||||||
await queryClient.invalidateQueries({ queryKey: ["projects"] });
|
|
||||||
await queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
|
||||||
navigate("/orders");
|
navigate("/orders");
|
||||||
} else {
|
} catch (e) {
|
||||||
alert.error(result.error || "Nepodařilo se smazat objednávku");
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
} finally {
|
||||||
setDeleting(false);
|
setDeleting(false);
|
||||||
setDeleteConfirm(false);
|
setDeleteConfirm(false);
|
||||||
@@ -288,12 +268,14 @@ export default function OrderDetail() {
|
|||||||
|
|
||||||
if (!order) return null;
|
if (!order) return null;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="admin-loading">
|
||||||
|
<div className="admin-spinner" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Skeleton
|
|
||||||
name="order-detail"
|
|
||||||
loading={loading}
|
|
||||||
fixture={<OrderDetailFixture />}
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -393,8 +375,8 @@ export default function OrderDetail() {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{hasPermission("orders.edit") &&
|
{hasPermission("orders.edit") &&
|
||||||
order.valid_transitions?.filter((s) => s !== "stornovana")
|
order.valid_transitions?.filter((s) => s !== "stornovana").length! >
|
||||||
.length! > 0 &&
|
0 &&
|
||||||
order
|
order
|
||||||
.valid_transitions!.filter((s) => s !== "stornovana")
|
.valid_transitions!.filter((s) => s !== "stornovana")
|
||||||
.map((status) => (
|
.map((status) => (
|
||||||
@@ -434,9 +416,9 @@ export default function OrderDetail() {
|
|||||||
>
|
>
|
||||||
<div className="admin-card-body">
|
<div className="admin-card-body">
|
||||||
<h3 className="admin-card-title">Informace</h3>
|
<h3 className="admin-card-title">Informace</h3>
|
||||||
<div className="admin-form-row mb-2">
|
<div className="admin-form-row admin-form-row-3 mb-2">
|
||||||
<FormField label="Nabídka">
|
<FormField label="Nabídka">
|
||||||
<div>
|
<div className="admin-mono fw-500">
|
||||||
<Link
|
<Link
|
||||||
to={`/offers/${order.quotation_id}`}
|
to={`/offers/${order.quotation_id}`}
|
||||||
className="link-accent"
|
className="link-accent"
|
||||||
@@ -454,7 +436,7 @@ export default function OrderDetail() {
|
|||||||
</div>
|
</div>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Projekt">
|
<FormField label="Projekt">
|
||||||
<div>
|
<div className="admin-mono fw-500">
|
||||||
{order.project ? (
|
{order.project ? (
|
||||||
<Link
|
<Link
|
||||||
to={`/projects/${order.project.id}`}
|
to={`/projects/${order.project.id}`}
|
||||||
@@ -467,21 +449,25 @@ export default function OrderDetail() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
<FormField label="Zákazník">
|
||||||
|
<div className="admin-mono fw-500">
|
||||||
|
{order.customer_name || "—"}
|
||||||
|
</div>
|
||||||
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-form-row admin-form-row-3 mb-2">
|
<div className="admin-form-row admin-form-row-3 mb-2">
|
||||||
<FormField label="Zákazník">
|
|
||||||
<div className="fw-500">{order.customer_name || "—"}</div>
|
|
||||||
</FormField>
|
|
||||||
<FormField label="Číslo obj. zákazníka">
|
<FormField label="Číslo obj. zákazníka">
|
||||||
<div>{order.customer_order_number || "—"}</div>
|
<div className="admin-mono">
|
||||||
|
{order.customer_order_number || "—"}
|
||||||
|
</div>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Měna">
|
<FormField label="Měna">
|
||||||
<div>{order.currency}</div>
|
<div className="admin-mono">{order.currency}</div>
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
<div className="admin-form-row admin-form-row-3 mb-2">
|
<div className="admin-form-row admin-form-row-3 mb-2">
|
||||||
<FormField label="Datum vytvoření">
|
<FormField label="Datum vytvoření">
|
||||||
<div>{formatDate(order.created_at)}</div>
|
<div className="admin-mono">{formatDate(order.created_at)}</div>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Příloha">
|
<FormField label="Příloha">
|
||||||
<div>
|
<div>
|
||||||
@@ -536,9 +522,7 @@ export default function OrderDetail() {
|
|||||||
<table className="admin-table">
|
<table className="admin-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style={{ width: "2.5rem", textAlign: "center" }}>
|
<th style={{ width: "2.5rem", textAlign: "center" }}>#</th>
|
||||||
#
|
|
||||||
</th>
|
|
||||||
<th>Popis</th>
|
<th>Popis</th>
|
||||||
<th style={{ width: "5.5rem", textAlign: "center" }}>
|
<th style={{ width: "5.5rem", textAlign: "center" }}>
|
||||||
Množství
|
Množství
|
||||||
@@ -601,9 +585,7 @@ export default function OrderDetail() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td style={{ textAlign: "center" }}>
|
<td style={{ textAlign: "center" }}>{item.quantity}</td>
|
||||||
{item.quantity}
|
|
||||||
</td>
|
|
||||||
<td style={{ textAlign: "center" }}>
|
<td style={{ textAlign: "center" }}>
|
||||||
{item.unit || "—"}
|
{item.unit || "—"}
|
||||||
</td>
|
</td>
|
||||||
@@ -645,9 +627,7 @@ export default function OrderDetail() {
|
|||||||
{Number(order.apply_vat) > 0 && (
|
{Number(order.apply_vat) > 0 && (
|
||||||
<div className="admin-totals-row">
|
<div className="admin-totals-row">
|
||||||
<span>DPH ({order.vat_rate}%):</span>
|
<span>DPH ({order.vat_rate}%):</span>
|
||||||
<span>
|
<span>{formatCurrency(totals.vatAmount, order.currency)}</span>
|
||||||
{formatCurrency(totals.vatAmount, order.currency)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="admin-totals-row admin-totals-total">
|
<div className="admin-totals-row admin-totals-total">
|
||||||
@@ -816,6 +796,5 @@ export default function OrderDetail() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Skeleton>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,15 @@ 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 { Skeleton } from "boneyard-js/react";
|
|
||||||
import OrdersFixture from "../fixtures/OrdersFixture";
|
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import ConfirmModal from "../components/ConfirmModal";
|
import ConfirmModal from "../components/ConfirmModal";
|
||||||
|
|
||||||
import apiFetch from "../utils/api";
|
|
||||||
import { formatCurrency, formatDate, czechPlural } from "../utils/formatters";
|
import { formatCurrency, 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 { useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { usePaginatedQuery } from "../hooks/usePaginatedQuery";
|
import { usePaginatedQuery } from "../hooks/usePaginatedQuery";
|
||||||
import { orderListOptions } from "../lib/queries/orders";
|
import { orderListOptions } from "../lib/queries/orders";
|
||||||
|
import { useApiMutation } from "../lib/queries/mutations";
|
||||||
import Pagination from "../components/Pagination";
|
import Pagination from "../components/Pagination";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
@@ -58,10 +55,22 @@ export default function Orders() {
|
|||||||
show: boolean;
|
show: boolean;
|
||||||
order: Order | null;
|
order: Order | null;
|
||||||
}>({ show: false, order: null });
|
}>({ show: false, order: null });
|
||||||
const [deleting, setDeleting] = useState(false);
|
|
||||||
const [deleteFiles, setDeleteFiles] = useState(false);
|
const [deleteFiles, setDeleteFiles] = useState(false);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const deleteMutation = useApiMutation<
|
||||||
|
{ id: number; delete_files: boolean },
|
||||||
|
{ message?: string; error?: string }
|
||||||
|
>({
|
||||||
|
url: ({ id }) => `${API_BASE}/orders/${id}`,
|
||||||
|
method: () => "DELETE",
|
||||||
|
invalidate: ["orders", "offers", "projects", "invoices"],
|
||||||
|
onSuccess: (data) => {
|
||||||
|
setDeleteConfirm({ show: false, order: null });
|
||||||
|
setDeleteFiles(false);
|
||||||
|
alert.success(data?.message || "Objednávka byla smazána");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
items: orders,
|
items: orders,
|
||||||
pagination,
|
pagination,
|
||||||
@@ -73,37 +82,25 @@ export default function Orders() {
|
|||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!deleteConfirm.order) return;
|
if (!deleteConfirm.order) return;
|
||||||
setDeleting(true);
|
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(
|
await deleteMutation.mutateAsync({
|
||||||
`${API_BASE}/orders/${deleteConfirm.order.id}`,
|
id: deleteConfirm.order.id,
|
||||||
{
|
delete_files: deleteFiles,
|
||||||
method: "DELETE",
|
});
|
||||||
headers: { "Content-Type": "application/json" },
|
} catch (e) {
|
||||||
body: JSON.stringify({ delete_files: deleteFiles }),
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
},
|
|
||||||
);
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setDeleteConfirm({ show: false, order: null });
|
|
||||||
setDeleteFiles(false);
|
|
||||||
alert.success(result.message || "Objednávka byla smazána");
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
|
||||||
} else {
|
|
||||||
alert.error(result.error || "Nepodařilo se smazat objednávku");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
|
||||||
setDeleting(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isPending) {
|
||||||
|
return (
|
||||||
|
<div className="admin-loading">
|
||||||
|
<div className="admin-spinner" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Skeleton name="orders" loading={isPending} fixture={<OrdersFixture />}>
|
|
||||||
<div>
|
<div>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="admin-page-header"
|
className="admin-page-header"
|
||||||
@@ -238,9 +235,7 @@ export default function Orders() {
|
|||||||
{STATUS_LABELS[o.status] || o.status}
|
{STATUS_LABELS[o.status] || o.status}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="admin-mono">
|
<td className="admin-mono">{formatDate(o.created_at)}</td>
|
||||||
{formatDate(o.created_at)}
|
|
||||||
</td>
|
|
||||||
<td className="admin-mono text-right fw-500">
|
<td className="admin-mono text-right fw-500">
|
||||||
{formatCurrency(o.total, o.currency)}
|
{formatCurrency(o.total, o.currency)}
|
||||||
</td>
|
</td>
|
||||||
@@ -362,8 +357,8 @@ export default function Orders() {
|
|||||||
message={
|
message={
|
||||||
<>
|
<>
|
||||||
Opravdu chcete smazat objednávku "
|
Opravdu chcete smazat objednávku "
|
||||||
{deleteConfirm.order?.order_number}"? Bude smazán i
|
{deleteConfirm.order?.order_number}"? Bude smazán i přidružený
|
||||||
přidružený projekt. Tato akce je nevratná.
|
projekt. Tato akce je nevratná.
|
||||||
<label
|
<label
|
||||||
className="admin-form-checkbox"
|
className="admin-form-checkbox"
|
||||||
style={{ marginTop: "1rem", display: "flex" }}
|
style={{ marginTop: "1rem", display: "flex" }}
|
||||||
@@ -380,9 +375,8 @@ export default function Orders() {
|
|||||||
confirmText="Smazat"
|
confirmText="Smazat"
|
||||||
cancelText="Zrušit"
|
cancelText="Zrušit"
|
||||||
type="danger"
|
type="danger"
|
||||||
loading={deleting}
|
loading={deleteMutation.isPending}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Skeleton>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useApiMutation } from "../lib/queries/mutations";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { useParams, useNavigate, Link } from "react-router-dom";
|
import { useParams, useNavigate, Link } from "react-router-dom";
|
||||||
@@ -12,8 +13,6 @@ import {
|
|||||||
} from "../lib/queries/projects";
|
} from "../lib/queries/projects";
|
||||||
import { userListOptions, type User as ApiUser } from "../lib/queries/users";
|
import { userListOptions, type User as ApiUser } from "../lib/queries/users";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
import { Skeleton } from "boneyard-js/react";
|
|
||||||
import ProjectDetailFixture from "../fixtures/ProjectDetailFixture";
|
|
||||||
import ConfirmModal from "../components/ConfirmModal";
|
import ConfirmModal from "../components/ConfirmModal";
|
||||||
import FormField from "../components/FormField";
|
import FormField from "../components/FormField";
|
||||||
import AdminDatePicker from "../components/AdminDatePicker";
|
import AdminDatePicker from "../components/AdminDatePicker";
|
||||||
@@ -76,7 +75,6 @@ export default function ProjectDetail() {
|
|||||||
const [addingNote, setAddingNote] = useState(false);
|
const [addingNote, setAddingNote] = useState(false);
|
||||||
const [deletingNoteId, setDeletingNoteId] = useState<number | null>(null);
|
const [deletingNoteId, setDeletingNoteId] = useState<number | null>(null);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const projectQuery = useQuery(projectDetailOptions(id));
|
const projectQuery = useQuery(projectDetailOptions(id));
|
||||||
const project = projectQuery.data;
|
const project = projectQuery.data;
|
||||||
const isPending = projectQuery.isPending;
|
const isPending = projectQuery.isPending;
|
||||||
@@ -118,6 +116,42 @@ export default function ProjectDetail() {
|
|||||||
|
|
||||||
if (!hasPermission("projects.view")) return <Forbidden />;
|
if (!hasPermission("projects.view")) return <Forbidden />;
|
||||||
|
|
||||||
|
const projectSaveMutation = useApiMutation<
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
status: string;
|
||||||
|
start_date: string | null;
|
||||||
|
end_date: string | null;
|
||||||
|
responsible_user_id: string | null;
|
||||||
|
},
|
||||||
|
unknown
|
||||||
|
>({
|
||||||
|
url: () => `${API_BASE}/projects/${id}`,
|
||||||
|
method: () => "PUT",
|
||||||
|
invalidate: ["projects", "warehouse"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectDeleteMutation = useApiMutation<
|
||||||
|
{ delete_files: boolean },
|
||||||
|
unknown
|
||||||
|
>({
|
||||||
|
url: () => `${API_BASE}/projects/${id}`,
|
||||||
|
method: () => "DELETE",
|
||||||
|
invalidate: ["projects", "warehouse"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectAddNoteMutation = useApiMutation<{ content: string }, unknown>({
|
||||||
|
url: () => `${API_BASE}/projects/${id}/notes`,
|
||||||
|
method: () => "POST",
|
||||||
|
invalidate: ["projects", "warehouse"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectDeleteNoteMutation = useApiMutation<number, unknown>({
|
||||||
|
url: (noteId) => `${API_BASE}/projects/${id}/notes/${noteId}`,
|
||||||
|
method: () => "DELETE",
|
||||||
|
invalidate: ["projects", "warehouse"],
|
||||||
|
});
|
||||||
|
|
||||||
const updateForm = (field: keyof ProjectForm, value: string) =>
|
const updateForm = (field: keyof ProjectForm, value: string) =>
|
||||||
setForm((prev) => ({ ...prev, [field]: value }));
|
setForm((prev) => ({ ...prev, [field]: value }));
|
||||||
|
|
||||||
@@ -129,30 +163,16 @@ export default function ProjectDetail() {
|
|||||||
|
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(`${API_BASE}/projects/${id}`, {
|
await projectSaveMutation.mutateAsync({
|
||||||
method: "PUT",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
name: form.name,
|
name: form.name,
|
||||||
status: form.status,
|
status: form.status,
|
||||||
start_date: form.start_date || null,
|
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,
|
responsible_user_id: form.responsible_user_id || null,
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
const result = await response.json();
|
alert.success("Projekt byl aktualizován");
|
||||||
if (result.success) {
|
} catch (e) {
|
||||||
alert.success(result.message || "Projekt byl aktualizován");
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["attendance"] });
|
|
||||||
} else {
|
|
||||||
alert.error(result.error || "Nepodařilo se uložit projekt");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -161,25 +181,11 @@ export default function ProjectDetail() {
|
|||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
setDeleting(true);
|
setDeleting(true);
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(`${API_BASE}/projects/${id}`, {
|
await projectDeleteMutation.mutateAsync({ delete_files: deleteFiles });
|
||||||
method: "DELETE",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ delete_files: deleteFiles }),
|
|
||||||
});
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["attendance"] });
|
|
||||||
navigate("/projects");
|
navigate("/projects");
|
||||||
setTimeout(() => alert.success("Projekt byl smazán"), 300);
|
setTimeout(() => alert.success("Projekt byl smazán"), 300);
|
||||||
} else {
|
} catch (e) {
|
||||||
alert.error(result.error || "Nepodařilo se smazat projekt");
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
} finally {
|
||||||
setDeleting(false);
|
setDeleting(false);
|
||||||
}
|
}
|
||||||
@@ -190,25 +196,11 @@ export default function ProjectDetail() {
|
|||||||
|
|
||||||
setAddingNote(true);
|
setAddingNote(true);
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(`${API_BASE}/projects/${id}/notes`, {
|
await projectAddNoteMutation.mutateAsync({ content: newNote.trim() });
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ content: newNote.trim() }),
|
|
||||||
});
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
setNewNote("");
|
setNewNote("");
|
||||||
alert.success("Poznámka byla přidána");
|
alert.success("Poznámka byla přidána");
|
||||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
} catch (e) {
|
||||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["attendance"] });
|
|
||||||
} else {
|
|
||||||
alert.error(result.error || "Nepodařilo se přidat poznámku");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
} finally {
|
||||||
setAddingNote(false);
|
setAddingNote(false);
|
||||||
}
|
}
|
||||||
@@ -217,25 +209,10 @@ export default function ProjectDetail() {
|
|||||||
const handleDeleteNote = async (noteId: number) => {
|
const handleDeleteNote = async (noteId: number) => {
|
||||||
setDeletingNoteId(noteId);
|
setDeletingNoteId(noteId);
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch(
|
await projectDeleteNoteMutation.mutateAsync(noteId);
|
||||||
`${API_BASE}/projects/${id}/notes/${noteId}`,
|
|
||||||
{
|
|
||||||
method: "DELETE",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
alert.success("Poznámka byla smazána");
|
alert.success("Poznámka byla smazána");
|
||||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
} catch (e) {
|
||||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["attendance"] });
|
|
||||||
} else {
|
|
||||||
alert.error(result.error || "Nepodařilo se smazat poznámku");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
} finally {
|
||||||
setDeletingNoteId(null);
|
setDeletingNoteId(null);
|
||||||
}
|
}
|
||||||
@@ -245,12 +222,15 @@ export default function ProjectDetail() {
|
|||||||
|
|
||||||
const canEdit = hasPermission("projects.edit");
|
const canEdit = hasPermission("projects.edit");
|
||||||
|
|
||||||
|
if (isPending) {
|
||||||
|
return (
|
||||||
|
<div className="admin-loading">
|
||||||
|
<div className="admin-spinner" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Skeleton
|
|
||||||
name="project-detail"
|
|
||||||
loading={isPending}
|
|
||||||
fixture={<ProjectDetailFixture />}
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -525,9 +505,7 @@ export default function ProjectDetail() {
|
|||||||
marginBottom: "0.25rem",
|
marginBottom: "0.25rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span style={{ fontWeight: 600, fontSize: "0.85rem" }}>
|
||||||
style={{ fontWeight: 600, fontSize: "0.85rem" }}
|
|
||||||
>
|
|
||||||
{note.user_name}
|
{note.user_name}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
@@ -690,6 +668,5 @@ export default function ProjectDetail() {
|
|||||||
loading={deleting}
|
loading={deleting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Skeleton>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,15 @@ 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 { Skeleton } from "boneyard-js/react";
|
|
||||||
import ProjectsFixture from "../fixtures/ProjectsFixture";
|
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import ConfirmModal from "../components/ConfirmModal";
|
import ConfirmModal from "../components/ConfirmModal";
|
||||||
|
|
||||||
import apiFetch from "../utils/api";
|
|
||||||
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 { useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { usePaginatedQuery } from "../hooks/usePaginatedQuery";
|
import { usePaginatedQuery } from "../hooks/usePaginatedQuery";
|
||||||
import { projectListOptions } from "../lib/queries/projects";
|
import { projectListOptions } from "../lib/queries/projects";
|
||||||
|
import { useApiMutation } from "../lib/queries/mutations";
|
||||||
import Pagination from "../components/Pagination";
|
import Pagination from "../components/Pagination";
|
||||||
|
|
||||||
const API_BASE = "/api/admin";
|
const API_BASE = "/api/admin";
|
||||||
@@ -52,11 +49,30 @@ export default function Projects() {
|
|||||||
useTableSort("project_number");
|
useTableSort("project_number");
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [deletingId, setDeletingId] = useState<number | null>(null);
|
|
||||||
const [deleteTarget, setDeleteTarget] = useState<Project | null>(null);
|
const [deleteTarget, setDeleteTarget] = useState<Project | null>(null);
|
||||||
const [deleteFiles, setDeleteFiles] = useState(false);
|
const [deleteFiles, setDeleteFiles] = useState(false);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const deleteMutation = useApiMutation<
|
||||||
|
{ id: number; delete_files: boolean },
|
||||||
|
{ message?: string; error?: string }
|
||||||
|
>({
|
||||||
|
url: ({ id }) => `${API_BASE}/projects/${id}`,
|
||||||
|
method: () => "DELETE",
|
||||||
|
invalidate: [
|
||||||
|
"projects",
|
||||||
|
"warehouse",
|
||||||
|
"orders",
|
||||||
|
"offers",
|
||||||
|
"invoices",
|
||||||
|
"attendance",
|
||||||
|
],
|
||||||
|
onSuccess: (data) => {
|
||||||
|
alert.success(data?.message || "Projekt byl smazán");
|
||||||
|
setDeleteTarget(null);
|
||||||
|
setDeleteFiles(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
items: projects,
|
items: projects,
|
||||||
pagination,
|
pagination,
|
||||||
@@ -70,35 +86,27 @@ export default function Projects() {
|
|||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!deleteTarget) return;
|
if (!deleteTarget) return;
|
||||||
setDeletingId(deleteTarget.id);
|
|
||||||
try {
|
try {
|
||||||
const res = await apiFetch(`${API_BASE}/projects/${deleteTarget.id}`, {
|
await deleteMutation.mutateAsync({
|
||||||
method: "DELETE",
|
id: deleteTarget.id,
|
||||||
headers: { "Content-Type": "application/json" },
|
delete_files: deleteFiles,
|
||||||
body: JSON.stringify({ delete_files: deleteFiles }),
|
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
} catch (e) {
|
||||||
if (data.success) {
|
alert.error(e instanceof Error ? e.message : "Chyba připojení");
|
||||||
alert.success(data.message || "Projekt byl smazán");
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["attendance"] });
|
|
||||||
} else {
|
|
||||||
alert.error(data.error || "Nepodařilo se smazat projekt");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
alert.error("Chyba připojení");
|
|
||||||
} finally {
|
|
||||||
setDeletingId(null);
|
|
||||||
setDeleteTarget(null);
|
setDeleteTarget(null);
|
||||||
setDeleteFiles(false);
|
setDeleteFiles(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isPending) {
|
||||||
|
return (
|
||||||
|
<div className="admin-loading">
|
||||||
|
<div className="admin-spinner" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Skeleton name="projects" loading={isPending} fixture={<ProjectsFixture />}>
|
|
||||||
<div>
|
<div>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="admin-page-header"
|
className="admin-page-header"
|
||||||
@@ -188,11 +196,7 @@ export default function Projects() {
|
|||||||
onClick={() => handleSort("name")}
|
onClick={() => handleSort("name")}
|
||||||
>
|
>
|
||||||
Název{" "}
|
Název{" "}
|
||||||
<SortIcon
|
<SortIcon column="name" sort={activeSort} order={order} />
|
||||||
column="name"
|
|
||||||
sort={activeSort}
|
|
||||||
order={order}
|
|
||||||
/>
|
|
||||||
</th>
|
</th>
|
||||||
<th>Zákazník</th>
|
<th>Zákazník</th>
|
||||||
<th>Zodpovědná osoba</th>
|
<th>Zodpovědná osoba</th>
|
||||||
@@ -237,10 +241,7 @@ export default function Projects() {
|
|||||||
{(projects as Project[]).map((p) => (
|
{(projects as Project[]).map((p) => (
|
||||||
<tr key={p.id}>
|
<tr key={p.id}>
|
||||||
<td className="admin-mono">
|
<td className="admin-mono">
|
||||||
<Link
|
<Link to={`/projects/${p.id}`} className="link-accent">
|
||||||
to={`/projects/${p.id}`}
|
|
||||||
className="link-accent"
|
|
||||||
>
|
|
||||||
{p.project_number}
|
{p.project_number}
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
@@ -254,9 +255,7 @@ export default function Projects() {
|
|||||||
{STATUS_LABELS[p.status] || p.status}
|
{STATUS_LABELS[p.status] || p.status}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="admin-mono">
|
<td className="admin-mono">{formatDate(p.start_date)}</td>
|
||||||
{formatDate(p.start_date)}
|
|
||||||
</td>
|
|
||||||
<td className="admin-mono">{formatDate(p.end_date)}</td>
|
<td className="admin-mono">{formatDate(p.end_date)}</td>
|
||||||
<td>
|
<td>
|
||||||
{p.order_id ? (
|
{p.order_id ? (
|
||||||
@@ -291,15 +290,18 @@ export default function Projects() {
|
|||||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
||||||
</svg>
|
</svg>
|
||||||
</Link>
|
</Link>
|
||||||
{!p.order_id &&
|
{!p.order_id && hasPermission("projects.delete") && (
|
||||||
hasPermission("projects.delete") && (
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setDeleteTarget(p)}
|
onClick={() => setDeleteTarget(p)}
|
||||||
className="admin-btn-icon danger"
|
className="admin-btn-icon danger"
|
||||||
title="Smazat projekt"
|
title="Smazat projekt"
|
||||||
disabled={deletingId === p.id}
|
disabled={
|
||||||
|
deleteMutation.isPending &&
|
||||||
|
deleteTarget?.id === p.id
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{deletingId === p.id ? (
|
{deleteMutation.isPending &&
|
||||||
|
deleteTarget?.id === p.id ? (
|
||||||
<div className="admin-spinner admin-spinner-sm" />
|
<div className="admin-spinner admin-spinner-sm" />
|
||||||
) : (
|
) : (
|
||||||
<svg
|
<svg
|
||||||
@@ -355,9 +357,8 @@ export default function Projects() {
|
|||||||
}
|
}
|
||||||
confirmText="Smazat"
|
confirmText="Smazat"
|
||||||
type="danger"
|
type="danger"
|
||||||
loading={!!deletingId}
|
loading={deleteMutation.isPending}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Skeleton>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user