v1.6.5: fix attendance unique constraint, schema sync, seed script
- Change attendance idx_attendance_user_date from unique to index (allow multiple shifts per day) - Reset migrations to single baseline init migration - Add seed script with admin user (admin/admin) - Update CLAUDE.md with migration workflow documentation - Various frontend fixes (queries, pages, hooks) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -14,8 +14,14 @@ import { motion, AnimatePresence } from "framer-motion";
|
||||
import {
|
||||
offerDetailOptions,
|
||||
offerCustomersOptions,
|
||||
offerTemplatesOptions,
|
||||
scopeTemplatesOptions,
|
||||
offerNextNumberOptions,
|
||||
type OfferDetailData,
|
||||
type OfferItemData,
|
||||
type OfferSectionData,
|
||||
type OfferLockInfo,
|
||||
type OfferOrderInfo,
|
||||
type ScopeTemplate,
|
||||
} from "../lib/queries/offers";
|
||||
|
||||
import {
|
||||
@@ -50,7 +56,10 @@ import apiFetch from "../utils/api";
|
||||
import { formatCurrency } from "../utils/formatters";
|
||||
import { Skeleton } from "boneyard-js/react";
|
||||
import OfferDetailFixture from "../fixtures/OfferDetailFixture";
|
||||
import { companySettingsOptions } from "../lib/queries/settings";
|
||||
import {
|
||||
companySettingsOptions,
|
||||
type CompanySettingsData,
|
||||
} from "../lib/queries/settings";
|
||||
|
||||
const API_BASE = "/api/admin";
|
||||
const DRAFT_KEY = "boha_offer_draft";
|
||||
@@ -94,11 +103,6 @@ interface Customer {
|
||||
city?: string;
|
||||
}
|
||||
|
||||
interface OrderInfo {
|
||||
id: number;
|
||||
order_number: string;
|
||||
}
|
||||
|
||||
const emptyForm: OfferForm = {
|
||||
quotation_number: "",
|
||||
project_code: "",
|
||||
@@ -206,7 +210,7 @@ function SortableItemRow({
|
||||
<input
|
||||
type="number"
|
||||
value={item.quantity}
|
||||
onChange={(e) => onUpdate("quantity", e.target.value)}
|
||||
onChange={(e) => onUpdate("quantity", parseInt(e.target.value, 10))}
|
||||
className="admin-form-input"
|
||||
step="1"
|
||||
readOnly={readOnly}
|
||||
@@ -225,7 +229,7 @@ function SortableItemRow({
|
||||
<input
|
||||
type="number"
|
||||
value={item.unit_price}
|
||||
onChange={(e) => onUpdate("unit_price", e.target.value)}
|
||||
onChange={(e) => onUpdate("unit_price", parseFloat(e.target.value))}
|
||||
className="admin-form-input"
|
||||
step="0.01"
|
||||
readOnly={readOnly}
|
||||
@@ -318,34 +322,15 @@ export default function OfferDetail() {
|
||||
...offerCustomersOptions(),
|
||||
enabled: !isEdit,
|
||||
});
|
||||
const { data: templatesData } = useQuery({
|
||||
...offerTemplatesOptions(),
|
||||
enabled: !isEdit,
|
||||
});
|
||||
const { data: templatesData } = useQuery(scopeTemplatesOptions());
|
||||
const { data: nextNumberData } = useQuery({
|
||||
...offerNextNumberOptions(),
|
||||
enabled: !isEdit,
|
||||
});
|
||||
// Derive typed arrays from query data
|
||||
const customers = (
|
||||
Array.isArray(customersData)
|
||||
? customersData
|
||||
: (customersData as Record<string, unknown>)?.customers
|
||||
? ((customersData as Record<string, unknown>).customers as Customer[])
|
||||
: []
|
||||
) as Customer[];
|
||||
const scopeTemplates = (
|
||||
Array.isArray(templatesData) ? templatesData : []
|
||||
) as Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
scope_template_sections?: Array<{
|
||||
title?: string;
|
||||
title_cz?: string;
|
||||
content?: string;
|
||||
}>;
|
||||
}>;
|
||||
const customers: Customer[] = Array.isArray(customersData)
|
||||
? customersData
|
||||
: [];
|
||||
const scopeTemplates = templatesData ?? [];
|
||||
|
||||
const loading = isEdit && offerQuery.isLoading;
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -396,7 +381,7 @@ export default function OfferDetail() {
|
||||
});
|
||||
const [customerSearch, setCustomerSearch] = useState("");
|
||||
const [showCustomerDropdown, setShowCustomerDropdown] = useState(false);
|
||||
const [orderInfo, setOrderInfo] = useState<OrderInfo | null>(null);
|
||||
const [orderInfo, setOrderInfo] = useState<OfferOrderInfo | null>(null);
|
||||
const [offerStatus, setOfferStatus] = useState<string>("");
|
||||
|
||||
const [deleteConfirm, setDeleteConfirm] = useState(false);
|
||||
@@ -409,14 +394,7 @@ export default function OfferDetail() {
|
||||
const [orderAttachment, setOrderAttachment] = useState<File | null>(null);
|
||||
const [pdfLoading, setPdfLoading] = useState(false);
|
||||
const blobTimeoutsRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
||||
const companySettings = useQuery(companySettingsOptions()).data as unknown as
|
||||
| {
|
||||
default_currency: string;
|
||||
default_vat_rate: number;
|
||||
available_currencies: string[];
|
||||
available_vat_rates: number[];
|
||||
}
|
||||
| undefined;
|
||||
const { data: companySettings } = useQuery(companySettingsOptions());
|
||||
|
||||
useEffect(() => {
|
||||
if (companySettings && !isEdit) {
|
||||
@@ -452,10 +430,13 @@ export default function OfferDetail() {
|
||||
}, []);
|
||||
|
||||
const isInvalidated = offerStatus === "invalidated";
|
||||
const isCompleted = orderInfo?.status === "dokoncena";
|
||||
const isLockedByOther = !!lockedBy;
|
||||
const readOnly = isInvalidated || isLockedByOther || isCompleted;
|
||||
const isExpiredNotInvalidated =
|
||||
isEdit &&
|
||||
!isInvalidated &&
|
||||
!isCompleted &&
|
||||
!orderInfo &&
|
||||
form.valid_until &&
|
||||
new Date(form.valid_until) < new Date(new Date().toDateString());
|
||||
@@ -464,28 +445,26 @@ export default function OfferDetail() {
|
||||
const formInitializedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!offerQuery.data || formInitializedRef.current) return;
|
||||
const d = offerQuery.data as Record<string, unknown>;
|
||||
const d = offerQuery.data;
|
||||
const formData: OfferForm = {
|
||||
quotation_number: (d.quotation_number as string) || "",
|
||||
project_code: (d.project_code as string) || "",
|
||||
customer_id: (d.customer_id as number | null) ?? null,
|
||||
customer_name: (d.customer_name as string) || "",
|
||||
created_at: d.created_at ? String(d.created_at).substring(0, 10) : "",
|
||||
valid_until: d.valid_until ? String(d.valid_until).substring(0, 10) : "",
|
||||
currency:
|
||||
(d.currency as string) || companySettings?.default_currency || "CZK",
|
||||
language: (d.language as string) || "EN",
|
||||
vat_rate:
|
||||
(d.vat_rate as number) ?? companySettings?.default_vat_rate ?? 21,
|
||||
quotation_number: d.quotation_number || "",
|
||||
project_code: d.project_code || "",
|
||||
customer_id: d.customer_id ?? null,
|
||||
customer_name: d.customer_name || "",
|
||||
created_at: d.created_at ? d.created_at.substring(0, 10) : "",
|
||||
valid_until: d.valid_until ? d.valid_until.substring(0, 10) : "",
|
||||
currency: d.currency || companySettings?.default_currency || "CZK",
|
||||
language: d.language || "EN",
|
||||
vat_rate: d.vat_rate ?? companySettings?.default_vat_rate ?? 21,
|
||||
apply_vat: !!d.apply_vat,
|
||||
exchange_rate: (d.exchange_rate as string) || "",
|
||||
scope_title: (d.scope_title as string) || "",
|
||||
scope_description: (d.scope_description as string) || "",
|
||||
exchange_rate: d.exchange_rate || "",
|
||||
scope_title: d.scope_title || "",
|
||||
scope_description: d.scope_description || "",
|
||||
};
|
||||
setForm(formData);
|
||||
const mappedItems =
|
||||
Array.isArray(d.items) && d.items.length
|
||||
? (d.items as any[]).map((it: any) => ({
|
||||
? d.items.map((it) => ({
|
||||
...it,
|
||||
_key: `item-${++itemKeyCounter.current}`,
|
||||
}))
|
||||
@@ -493,7 +472,7 @@ export default function OfferDetail() {
|
||||
setItems(mappedItems);
|
||||
const mappedSections =
|
||||
Array.isArray(d.sections) && d.sections.length
|
||||
? (d.sections as any[]).map((s: any) => ({
|
||||
? d.sections.map((s) => ({
|
||||
title: s.title || "",
|
||||
title_cz: s.title_cz || "",
|
||||
content: s.content || "",
|
||||
@@ -505,14 +484,15 @@ export default function OfferDetail() {
|
||||
items: mappedItems,
|
||||
sections: mappedSections,
|
||||
});
|
||||
setOfferStatus((d.status as string) || "");
|
||||
setOrderInfo((d.order as OrderInfo) ?? null);
|
||||
setLockedBy((d.locked_by as typeof lockedBy) ?? null);
|
||||
setOfferStatus(d.status || "");
|
||||
setOrderInfo(d.order ?? null);
|
||||
setLockedBy(d.locked_by ?? null);
|
||||
|
||||
// Try to acquire lock if not locked by someone else and not invalidated
|
||||
// Try to acquire lock if not locked by someone else, not invalidated, and not completed
|
||||
if (
|
||||
!d.locked_by &&
|
||||
d.status !== "invalidated" &&
|
||||
d.order?.status !== "dokoncena" &&
|
||||
hasPermission("offers.edit")
|
||||
) {
|
||||
apiFetch(`${API_BASE}/offers/${id}/lock`, { method: "POST" }).catch(
|
||||
@@ -533,10 +513,7 @@ export default function OfferDetail() {
|
||||
// Sync next-number data to form (create mode)
|
||||
useEffect(() => {
|
||||
if (isEdit || !nextNumberData) return;
|
||||
const num =
|
||||
((nextNumberData as Record<string, unknown>).next_number as string) ||
|
||||
((nextNumberData as Record<string, unknown>).number as string) ||
|
||||
"";
|
||||
const num = nextNumberData.next_number || nextNumberData.number || "";
|
||||
if (num) {
|
||||
setForm((prev) => ({ ...prev, quotation_number: num }));
|
||||
}
|
||||
@@ -544,7 +521,8 @@ export default function OfferDetail() {
|
||||
|
||||
// Heartbeat to keep lock alive + cleanup on unmount
|
||||
useEffect(() => {
|
||||
if (!isEdit || !id || isLockedByOther || isInvalidated) return;
|
||||
if (!isEdit || !id || isLockedByOther || isInvalidated || isCompleted)
|
||||
return;
|
||||
|
||||
heartbeatRef.current = setInterval(() => {
|
||||
apiFetch(`${API_BASE}/offers/${id}/heartbeat`, { method: "POST" }).catch(
|
||||
@@ -719,6 +697,10 @@ export default function OfferDetail() {
|
||||
}
|
||||
}
|
||||
if (!isEdit && result.data?.id) {
|
||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||
navigate(`/offers/${result.data.id}`);
|
||||
}
|
||||
if (isEdit) {
|
||||
@@ -730,6 +712,7 @@ export default function OfferDetail() {
|
||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||
}
|
||||
} else {
|
||||
alert.error(result.error || "Nepodařilo se uložit nabídku");
|
||||
@@ -772,9 +755,10 @@ export default function OfferDetail() {
|
||||
if (result.success) {
|
||||
setShowOrderModal(false);
|
||||
alert.success(result.message || "Objednávka byla vytvořena");
|
||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["offers"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||
navigate(`/orders/${result.data.order_id}`);
|
||||
} else {
|
||||
alert.error(result.error || "Nepodařilo se vytvořit objednávku");
|
||||
@@ -800,6 +784,7 @@ export default function OfferDetail() {
|
||||
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");
|
||||
}
|
||||
@@ -819,9 +804,10 @@ export default function OfferDetail() {
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
alert.success(result.message || "Nabídka byla smazána");
|
||||
queryClient.invalidateQueries({ queryKey: ["offers"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["offers"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["orders"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||
await queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
||||
navigate("/offers");
|
||||
} else {
|
||||
alert.error(result.error || "Nepodařilo se smazat nabídku");
|
||||
@@ -864,7 +850,7 @@ export default function OfferDetail() {
|
||||
|
||||
const getRequiredPerm = () => {
|
||||
if (!isEdit) return "offers.create";
|
||||
return isInvalidated ? "offers.view" : "offers.edit";
|
||||
return isInvalidated || isCompleted ? "offers.view" : "offers.edit";
|
||||
};
|
||||
const requiredPerm = getRequiredPerm();
|
||||
if (!hasPermission(requiredPerm)) return <Forbidden />;
|
||||
@@ -915,6 +901,17 @@ export default function OfferDetail() {
|
||||
Zneplatněna
|
||||
</span>
|
||||
)}
|
||||
{isCompleted && (
|
||||
<span
|
||||
className="admin-badge admin-badge-success text-xs"
|
||||
style={{
|
||||
marginLeft: "0.75rem",
|
||||
verticalAlign: "middle",
|
||||
}}
|
||||
>
|
||||
Dokončeno
|
||||
</span>
|
||||
)}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
@@ -949,7 +946,7 @@ export default function OfferDetail() {
|
||||
</button>
|
||||
)}
|
||||
{isEdit &&
|
||||
!isInvalidated &&
|
||||
!readOnly &&
|
||||
hasPermission("orders.create") &&
|
||||
!orderInfo && (
|
||||
<button
|
||||
@@ -979,7 +976,7 @@ export default function OfferDetail() {
|
||||
Zneplatnit
|
||||
</button>
|
||||
)}
|
||||
{!isInvalidated && !isLockedByOther && (
|
||||
{!readOnly && (
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="admin-btn admin-btn-primary"
|
||||
@@ -1042,7 +1039,7 @@ export default function OfferDetail() {
|
||||
|
||||
{/* Quotation Form */}
|
||||
<motion.div
|
||||
className={`admin-editor-section${isInvalidated || isLockedByOther ? " offers-readonly" : ""}`}
|
||||
className={`admin-editor-section${readOnly ? " offers-readonly" : ""}`}
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25, delay: 0.06 }}
|
||||
@@ -1069,14 +1066,14 @@ export default function OfferDetail() {
|
||||
onChange={(e) => updateForm("project_code", e.target.value)}
|
||||
className="admin-form-input"
|
||||
placeholder="Volitelný kód projektu"
|
||||
readOnly={isInvalidated || isLockedByOther}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Zákazník" error={errors.customer_id} required>
|
||||
{form.customer_id ? (
|
||||
<div className="admin-customer-selected">
|
||||
<span>{form.customer_name}</span>
|
||||
{!isInvalidated && !isLockedByOther && (
|
||||
{!readOnly && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearCustomer}
|
||||
@@ -1112,7 +1109,7 @@ export default function OfferDetail() {
|
||||
onFocus={() => setShowCustomerDropdown(true)}
|
||||
className="admin-form-input"
|
||||
placeholder="Hledat zákazníka..."
|
||||
readOnly={isInvalidated || isLockedByOther}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
{showCustomerDropdown && !isInvalidated && (
|
||||
<div className="admin-customer-dropdown">
|
||||
@@ -1145,7 +1142,7 @@ export default function OfferDetail() {
|
||||
error={errors.created_at}
|
||||
required
|
||||
>
|
||||
{isInvalidated || isLockedByOther ? (
|
||||
{readOnly ? (
|
||||
<input
|
||||
type="text"
|
||||
value={form.created_at}
|
||||
@@ -1168,7 +1165,7 @@ export default function OfferDetail() {
|
||||
error={errors.valid_until}
|
||||
required
|
||||
>
|
||||
{isInvalidated || isLockedByOther ? (
|
||||
{readOnly ? (
|
||||
<input
|
||||
type="text"
|
||||
value={form.valid_until}
|
||||
@@ -1197,7 +1194,7 @@ export default function OfferDetail() {
|
||||
value={form.currency}
|
||||
onChange={(e) => updateForm("currency", e.target.value)}
|
||||
className="admin-form-select"
|
||||
disabled={isInvalidated || isLockedByOther}
|
||||
disabled={readOnly}
|
||||
>
|
||||
{(
|
||||
companySettings?.available_currencies || [
|
||||
@@ -1218,7 +1215,7 @@ export default function OfferDetail() {
|
||||
value={form.language}
|
||||
onChange={(e) => updateForm("language", e.target.value)}
|
||||
className="admin-form-select"
|
||||
disabled={isInvalidated || isLockedByOther}
|
||||
disabled={readOnly}
|
||||
>
|
||||
<option value="EN">English</option>
|
||||
<option value="CZ">Čeština</option>
|
||||
@@ -1235,7 +1232,7 @@ export default function OfferDetail() {
|
||||
updateForm("vat_rate", parseFloat(e.target.value) || 0)
|
||||
}
|
||||
className="admin-form-select flex-1"
|
||||
disabled={isInvalidated || isLockedByOther}
|
||||
disabled={readOnly}
|
||||
>
|
||||
{(
|
||||
companySettings?.available_vat_rates || [
|
||||
@@ -1254,7 +1251,7 @@ export default function OfferDetail() {
|
||||
onChange={(e) =>
|
||||
updateForm("apply_vat", e.target.checked)
|
||||
}
|
||||
disabled={isInvalidated || isLockedByOther}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
<span>Účtovat DPH</span>
|
||||
</label>
|
||||
@@ -1268,7 +1265,7 @@ export default function OfferDetail() {
|
||||
className="admin-form-input"
|
||||
placeholder="Volitelný"
|
||||
step="0.0001"
|
||||
readOnly={isInvalidated || isLockedByOther}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
@@ -1285,7 +1282,7 @@ export default function OfferDetail() {
|
||||
<div className="admin-card-body">
|
||||
<div className="admin-card-header flex-between">
|
||||
<h3 className="admin-card-title">Položky</h3>
|
||||
{!isInvalidated && !isLockedByOther && (
|
||||
{!readOnly && (
|
||||
<button
|
||||
onClick={addItem}
|
||||
className="admin-btn admin-btn-secondary admin-btn-sm"
|
||||
@@ -1327,9 +1324,7 @@ export default function OfferDetail() {
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{!isInvalidated && !isLockedByOther && (
|
||||
<th style={{ width: "2rem" }} />
|
||||
)}
|
||||
{!readOnly && <th style={{ width: "2rem" }} />}
|
||||
<th style={{ width: "2.5rem", textAlign: "center" }}>
|
||||
#
|
||||
</th>
|
||||
@@ -1364,7 +1359,7 @@ export default function OfferDetail() {
|
||||
>
|
||||
Celkem
|
||||
</th>
|
||||
{!isInvalidated && !isLockedByOther && (
|
||||
{!readOnly && (
|
||||
<th
|
||||
className="offers-col-del"
|
||||
style={{ width: "3rem" }}
|
||||
@@ -1379,7 +1374,7 @@ export default function OfferDetail() {
|
||||
item={item}
|
||||
index={index}
|
||||
currency={form.currency}
|
||||
readOnly={isInvalidated || isLockedByOther}
|
||||
readOnly={readOnly}
|
||||
canDelete={items.length > 1}
|
||||
onUpdate={(field, value) =>
|
||||
updateItem(index, field, value)
|
||||
@@ -1423,7 +1418,7 @@ export default function OfferDetail() {
|
||||
<div className="admin-card-body">
|
||||
<div className="admin-card-header flex-between">
|
||||
<h3 className="admin-card-title">Rozsah projektu</h3>
|
||||
{!isInvalidated && !isLockedByOther && (
|
||||
{!readOnly && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
@@ -1444,7 +1439,7 @@ export default function OfferDetail() {
|
||||
);
|
||||
if (template?.scope_template_sections?.length) {
|
||||
const newSections =
|
||||
template.scope_template_sections.map((s: any) => ({
|
||||
template.scope_template_sections.map((s) => ({
|
||||
title: s.title || "",
|
||||
title_cz: s.title_cz || "",
|
||||
content: s.content || "",
|
||||
@@ -1537,7 +1532,7 @@ export default function OfferDetail() {
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{!isInvalidated && !isLockedByOther && (
|
||||
{!readOnly && (
|
||||
<div style={{ display: "flex", gap: "0.25rem" }}>
|
||||
{idx > 0 && (
|
||||
<button
|
||||
@@ -1639,7 +1634,7 @@ export default function OfferDetail() {
|
||||
}
|
||||
className="admin-form-input"
|
||||
placeholder="Název sekce (anglicky)"
|
||||
readOnly={isInvalidated || isLockedByOther}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
@@ -1666,7 +1661,7 @@ export default function OfferDetail() {
|
||||
}
|
||||
className="admin-form-input"
|
||||
placeholder="Název sekce (česky)"
|
||||
readOnly={isInvalidated || isLockedByOther}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
@@ -1684,7 +1679,7 @@ export default function OfferDetail() {
|
||||
}
|
||||
placeholder="Obsah sekce..."
|
||||
minHeight="120px"
|
||||
readOnly={isInvalidated || isLockedByOther}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user