Aufgaben ohne Kunde/Vertrag anlegbar
ContractTask.contractId nullable (Migration). Neuer staff-only Endpoint POST /tasks fuer allgemeine Aufgaben ohne Vertrag/Kunde. Ohne Vertrag gibt es keinen Kunden -> visibleInPortal serverseitig immer false, Portal-Reply 403 bei contractloser Aufgabe, getAllTasks-Portal-Filter schliesst sie automatisch aus (kein contract-Match). Task-Modal (Mitarbeiter): Checkbox "Ohne Kunde (allgemeine Aufgabe)" blendet Kunden-/Vertragsauswahl UND "Im Kundenportal sichtbar" aus. Task-Liste zeigt solche Aufgaben als "Allgemeine Aufgabe (ohne Vertrag)" ohne Vertrags-Link/ Zum-Vertrag-Button. Verifiziert: contractlose Aufgabe -> contractId null, visibleInPortal erzwungen false (auch wenn true geschickt); mit Vertrag weiterhin waehlbar. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
-- Aufgaben ohne Vertrag/Kunde erlauben: contractId nullable.
|
||||||
|
-- Der bestehende FK (onDelete Cascade) bleibt: gesetzte contractId cascaden
|
||||||
|
-- weiterhin, NULL bleibt unberührt.
|
||||||
|
ALTER TABLE `ContractTask` MODIFY COLUMN `contractId` INT NULL;
|
||||||
@@ -870,8 +870,10 @@ enum ContractTaskStatus {
|
|||||||
|
|
||||||
model ContractTask {
|
model ContractTask {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
contractId Int
|
// Nullable: Aufgaben können auch OHNE Vertrag/Kunde angelegt werden
|
||||||
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
|
// (allgemeine interne Aufgabe). Ohne Vertrag → kein Kunde → nie im Portal.
|
||||||
|
contractId Int?
|
||||||
|
contract Contract? @relation(fields: [contractId], references: [id], onDelete: Cascade)
|
||||||
title String
|
title String
|
||||||
description String? @db.Text
|
description String? @db.Text
|
||||||
status ContractTaskStatus @default(OPEN)
|
status ContractTaskStatus @default(OPEN)
|
||||||
|
|||||||
@@ -118,6 +118,41 @@ export async function getTasks(req: AuthRequest, res: Response): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allgemeine Aufgabe OHNE Vertrag/Kunde (nur Mitarbeiter/Admin). Ohne Vertrag
|
||||||
|
// gibt es keinen Kunden → visibleInPortal ist hier bedeutungslos und immer
|
||||||
|
// false (der Service erzwingt das ohnehin).
|
||||||
|
export async function createGeneralTask(req: AuthRequest, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (req.user?.isCustomerPortal) {
|
||||||
|
res.status(403).json({ success: false, error: 'Kein Zugriff' } as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { title, description } = req.body ?? {};
|
||||||
|
if (!title || !String(title).trim()) {
|
||||||
|
res.status(400).json({ success: false, error: 'Titel ist erforderlich' } as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const task = await contractTaskService.createTask({
|
||||||
|
contractId: null,
|
||||||
|
title: String(title).trim(),
|
||||||
|
description,
|
||||||
|
visibleInPortal: false,
|
||||||
|
createdBy: req.user?.email,
|
||||||
|
});
|
||||||
|
await logChange({
|
||||||
|
req, action: 'CREATE', resourceType: 'ContractTask',
|
||||||
|
resourceId: task.id.toString(),
|
||||||
|
label: `Allgemeine Aufgabe "${String(title).trim()}" erstellt (ohne Vertrag)`,
|
||||||
|
});
|
||||||
|
res.status(201).json({ success: true, data: task } as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Aufgabe',
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function createTask(req: AuthRequest, res: Response): Promise<void> {
|
export async function createTask(req: AuthRequest, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const contractId = parseInt(req.params.contractId);
|
const contractId = parseInt(req.params.contractId);
|
||||||
@@ -361,6 +396,12 @@ export async function createCustomerReply(req: AuthRequest, res: Response): Prom
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allgemeine Aufgaben (ohne Vertrag/Kunde) sind reine Mitarbeiter-Aufgaben
|
||||||
|
// und für Portal-User nie zugänglich.
|
||||||
|
if (task.contractId == null) {
|
||||||
|
res.status(403).json({ success: false, error: 'Kein Zugriff' } as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Strikter Owner-Check über den Vertrag (mit Live-Vollmacht-Prüfung
|
// Strikter Owner-Check über den Vertrag (mit Live-Vollmacht-Prüfung
|
||||||
// via hasAuthorization, Pentest Runde 6 – HOCH-04). Damit kann ein
|
// via hasAuthorization, Pentest Runde 6 – HOCH-04). Damit kann ein
|
||||||
// Portal-User keine fremde Task-ID mit visibleInPortal=true abgreifen.
|
// Portal-User keine fremde Task-ID mit visibleInPortal=true abgreifen.
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ router.get(
|
|||||||
contractTaskController.getTaskStats
|
contractTaskController.getTaskStats
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Allgemeine Aufgabe OHNE Vertrag/Kunde anlegen (nur Mitarbeiter)
|
||||||
|
router.post(
|
||||||
|
'/tasks',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('contracts:update'),
|
||||||
|
contractTaskController.createGeneralTask
|
||||||
|
);
|
||||||
|
|
||||||
// ==================== TASKS BY CONTRACT ====================
|
// ==================== TASKS BY CONTRACT ====================
|
||||||
|
|
||||||
// Alle Aufgaben eines Vertrags abrufen (auch für Kundenportal, aber nur sichtbare)
|
// Alle Aufgaben eines Vertrags abrufen (auch für Kundenportal, aber nur sichtbare)
|
||||||
|
|||||||
@@ -50,18 +50,21 @@ export async function getTaskById(id: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createTask(data: {
|
export async function createTask(data: {
|
||||||
contractId: number;
|
contractId?: number | null;
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
visibleInPortal?: boolean;
|
visibleInPortal?: boolean;
|
||||||
createdBy?: string;
|
createdBy?: string;
|
||||||
}) {
|
}) {
|
||||||
|
// Ohne Vertrag (allgemeine Aufgabe) gibt es keinen Kunden → nie im Portal
|
||||||
|
// sichtbar, egal was der Client schickt.
|
||||||
|
const hasContract = data.contractId != null;
|
||||||
return prisma.contractTask.create({
|
return prisma.contractTask.create({
|
||||||
data: {
|
data: {
|
||||||
contractId: data.contractId,
|
contractId: data.contractId ?? null,
|
||||||
title: data.title,
|
title: data.title,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
visibleInPortal: data.visibleInPortal ?? false,
|
visibleInPortal: hasContract ? (data.visibleInPortal ?? false) : false,
|
||||||
createdBy: data.createdBy,
|
createdBy: data.createdBy,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -97,6 +97,27 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
|
|
||||||
## ✅ Erledigt
|
## ✅ Erledigt
|
||||||
|
|
||||||
|
- [x] **📋 Aufgaben ohne Kunde/Vertrag anlegbar** (2026-08-18)
|
||||||
|
- `ContractTask.contractId` nullable (Migration `20260818110000`). Neuer Endpoint
|
||||||
|
`POST /tasks` (staff-only, `contracts:update`) für allgemeine Aufgaben ohne
|
||||||
|
Vertrag/Kunde. Ohne Vertrag → **kein Kunde → nie im Portal sichtbar**
|
||||||
|
(`visibleInPortal` serverseitig erzwungen false; Portal-Reply-Endpoint 403 bei
|
||||||
|
contractloser Aufgabe; getAllTasks-Portal-Filter schließt sie automatisch aus).
|
||||||
|
- Task-Modal (Mitarbeiter): Checkbox „Ohne Kunde (allgemeine Aufgabe)" – blendet
|
||||||
|
Kunden-/Vertragsauswahl **und** „Im Kundenportal sichtbar" aus. Task-Liste zeigt
|
||||||
|
solche Aufgaben als „Allgemeine Aufgabe (ohne Vertrag)" (kein Vertrags-Link).
|
||||||
|
|
||||||
|
- [x] **📧 Kunde: E-Mail Pflichtfeld + keine verwaltete Provider-Domain** (2026-08-18)
|
||||||
|
- Private Kunden-E-Mail (`Customer.email`) darf nicht auf einer bei den E-Mail-
|
||||||
|
Providern konfigurierten Domain (oder Subdomain) liegen → man trägt so keine
|
||||||
|
verwaltete Weiterleitungs-/Mailbox-Adresse als private Adresse ein. E-Mail ist
|
||||||
|
jetzt Pflichtfeld (Frontend `required` + Backend create/update).
|
||||||
|
- Helper `getConfiguredEmailDomains`/`emailUsesDomain` im emailProvider-Service.
|
||||||
|
|
||||||
|
- [x] **⚡ Energievertrag: Ankreuzfeld „Keine Bonis erwünscht"** (2026-08-18)
|
||||||
|
- `EnergyContractDetails.noBonusDesired` (Boolean, Migration `20260818100000`).
|
||||||
|
Checkbox im Vertragsformular (Strom/Gas), Anzeige im Vertragsdetail.
|
||||||
|
|
||||||
- [x] **🔌 MaLo-ID (Marktlokation) an die Lieferadresse verschoben (Strom/Gas)** (2026-08-14)
|
- [x] **🔌 MaLo-ID (Marktlokation) an die Lieferadresse verschoben (Strom/Gas)** (2026-08-14)
|
||||||
- MaLo-ID gehört zur **(Liefer-)Adresse**, nicht zum Vertrag. Adresse bekommt
|
- MaLo-ID gehört zur **(Liefer-)Adresse**, nicht zum Vertrag. Adresse bekommt
|
||||||
**zwei Felder**: `maloIdElectricity` (Strom) + `maloIdGas` (Gas) – im AddressModal
|
**zwei Felder**: `maloIdElectricity` (Strom) + `maloIdGas` (Gas) – im AddressModal
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ export default function TaskList() {
|
|||||||
|
|
||||||
const contractDisplay = task.contract
|
const contractDisplay = task.contract
|
||||||
? `${task.contract.contractNumber} - ${task.contract.provider?.name || task.contract.providerName || 'Kein Anbieter'}`
|
? `${task.contract.contractNumber} - ${task.contract.provider?.name || task.contract.providerName || 'Kein Anbieter'}`
|
||||||
: `Vertrag #${task.contractId}`;
|
: (task.contractId != null ? `Vertrag #${task.contractId}` : 'Allgemeine Aufgabe (ohne Vertrag)');
|
||||||
|
|
||||||
const customerDisplay = task.contract?.customer
|
const customerDisplay = task.contract?.customer
|
||||||
? (task.contract.customer.companyName || `${task.contract.customer.firstName} ${task.contract.customer.lastName}`)
|
? (task.contract.customer.companyName || `${task.contract.customer.firstName} ${task.contract.customer.lastName}`)
|
||||||
@@ -242,6 +242,7 @@ export default function TaskList() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-500 mt-1 flex items-center gap-2">
|
<div className="text-sm text-gray-500 mt-1 flex items-center gap-2">
|
||||||
<FileText className="w-4 h-4" />
|
<FileText className="w-4 h-4" />
|
||||||
|
{task.contractId != null ? (
|
||||||
<Link
|
<Link
|
||||||
to={`/contracts/${task.contractId}`}
|
to={`/contracts/${task.contractId}`}
|
||||||
className="text-blue-600 hover:underline"
|
className="text-blue-600 hover:underline"
|
||||||
@@ -249,6 +250,9 @@ export default function TaskList() {
|
|||||||
>
|
>
|
||||||
{contractDisplay}
|
{contractDisplay}
|
||||||
</Link>
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-500 italic">{contractDisplay}</span>
|
||||||
|
)}
|
||||||
{showCustomer && customerDisplay && (
|
{showCustomer && customerDisplay && (
|
||||||
<>
|
<>
|
||||||
<span className="text-gray-400">|</span>
|
<span className="text-gray-400">|</span>
|
||||||
@@ -296,6 +300,7 @@ export default function TaskList() {
|
|||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{task.contractId != null && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -307,6 +312,7 @@ export default function TaskList() {
|
|||||||
>
|
>
|
||||||
<Eye className="w-4 h-4" />
|
<Eye className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -763,6 +769,7 @@ function CreateTaskModal({
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [selectedCustomerId, setSelectedCustomerId] = useState<number | null>(null);
|
const [selectedCustomerId, setSelectedCustomerId] = useState<number | null>(null);
|
||||||
const [selectedContractId, setSelectedContractId] = useState<number | null>(null);
|
const [selectedContractId, setSelectedContractId] = useState<number | null>(null);
|
||||||
|
const [noCustomer, setNoCustomer] = useState(false);
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
const [description, setDescription] = useState('');
|
const [description, setDescription] = useState('');
|
||||||
const [visibleInPortal, setVisibleInPortal] = useState(false);
|
const [visibleInPortal, setVisibleInPortal] = useState(false);
|
||||||
@@ -810,15 +817,25 @@ function CreateTaskModal({
|
|||||||
}, [contractsData?.data, contractSearch]);
|
}, [contractsData?.data, contractSearch]);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!selectedContractId || !title.trim()) return;
|
if (!title.trim()) return;
|
||||||
|
if (!noCustomer && !selectedContractId) return;
|
||||||
|
|
||||||
|
const contractId = selectedContractId;
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
await contractTaskApi.create(selectedContractId, {
|
if (noCustomer) {
|
||||||
|
// Allgemeine Aufgabe ohne Vertrag/Kunde – nie im Portal sichtbar.
|
||||||
|
await contractTaskApi.createGeneral({
|
||||||
|
title: title.trim(),
|
||||||
|
description: description.trim() || undefined,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await contractTaskApi.create(contractId!, {
|
||||||
title: title.trim(),
|
title: title.trim(),
|
||||||
description: description.trim() || undefined,
|
description: description.trim() || undefined,
|
||||||
visibleInPortal,
|
visibleInPortal,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
queryClient.invalidateQueries({ queryKey: ['all-tasks'] });
|
queryClient.invalidateQueries({ queryKey: ['all-tasks'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['task-stats'] });
|
queryClient.invalidateQueries({ queryKey: ['task-stats'] });
|
||||||
onClose();
|
onClose();
|
||||||
@@ -828,8 +845,9 @@ function CreateTaskModal({
|
|||||||
setVisibleInPortal(false);
|
setVisibleInPortal(false);
|
||||||
setSelectedContractId(null);
|
setSelectedContractId(null);
|
||||||
setSelectedCustomerId(null);
|
setSelectedCustomerId(null);
|
||||||
// Navigate to the contract
|
setNoCustomer(false);
|
||||||
navigate(`/contracts/${selectedContractId}`);
|
// Bei vertragsgebundener Aufgabe zum Vertrag springen.
|
||||||
|
if (!noCustomer && contractId) navigate(`/contracts/${contractId}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Erstellen der Aufgabe:', error);
|
console.error('Fehler beim Erstellen der Aufgabe:', error);
|
||||||
alert('Fehler beim Erstellen der Aufgabe. Bitte versuchen Sie es erneut.');
|
alert('Fehler beim Erstellen der Aufgabe. Bitte versuchen Sie es erneut.');
|
||||||
@@ -844,6 +862,7 @@ function CreateTaskModal({
|
|||||||
setVisibleInPortal(false);
|
setVisibleInPortal(false);
|
||||||
setSelectedContractId(null);
|
setSelectedContractId(null);
|
||||||
setSelectedCustomerId(null);
|
setSelectedCustomerId(null);
|
||||||
|
setNoCustomer(false);
|
||||||
setCustomerSearch('');
|
setCustomerSearch('');
|
||||||
setContractSearch('');
|
setContractSearch('');
|
||||||
onClose();
|
onClose();
|
||||||
@@ -861,7 +880,27 @@ function CreateTaskModal({
|
|||||||
title="Neue Aufgabe"
|
title="Neue Aufgabe"
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{/* Ohne Kunde (allgemeine Aufgabe) */}
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={noCustomer}
|
||||||
|
onChange={(e) => {
|
||||||
|
setNoCustomer(e.target.checked);
|
||||||
|
if (e.target.checked) {
|
||||||
|
// Ohne Kunde → Kunden-/Vertragsauswahl + Portal-Sichtbarkeit weg.
|
||||||
|
setSelectedCustomerId(null);
|
||||||
|
setSelectedContractId(null);
|
||||||
|
setVisibleInPortal(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700">Ohne Kunde (allgemeine Aufgabe)</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
{/* Kundenauswahl */}
|
{/* Kundenauswahl */}
|
||||||
|
{!noCustomer && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Kunde *
|
Kunde *
|
||||||
@@ -896,6 +935,7 @@ function CreateTaskModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Vertragsauswahl (nur wenn Kunde ausgewählt) */}
|
{/* Vertragsauswahl (nur wenn Kunde ausgewählt) */}
|
||||||
{selectedCustomerId && (
|
{selectedCustomerId && (
|
||||||
@@ -969,7 +1009,8 @@ function CreateTaskModal({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Im Kundenportal sichtbar */}
|
{/* Im Kundenportal sichtbar – nur wenn ein Kunde/Vertrag zugeordnet ist */}
|
||||||
|
{!noCustomer && (
|
||||||
<div>
|
<div>
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
@@ -981,6 +1022,7 @@ function CreateTaskModal({
|
|||||||
<span className="text-sm text-gray-700">Im Kundenportal sichtbar</span>
|
<span className="text-sm text-gray-700">Im Kundenportal sichtbar</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Buttons */}
|
{/* Buttons */}
|
||||||
<div className="flex justify-end gap-2 pt-4">
|
<div className="flex justify-end gap-2 pt-4">
|
||||||
@@ -989,7 +1031,7 @@ function CreateTaskModal({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={!selectedContractId || !title.trim() || isSubmitting}
|
disabled={(!noCustomer && !selectedContractId) || !title.trim() || isSubmitting}
|
||||||
>
|
>
|
||||||
{isSubmitting ? 'Wird erstellt...' : 'Aufgabe erstellen'}
|
{isSubmitting ? 'Wird erstellt...' : 'Aufgabe erstellen'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1072,6 +1072,11 @@ export const contractTaskApi = {
|
|||||||
const res = await api.post<ApiResponse<ContractTask>>(`/contracts/${contractId}/tasks`, data);
|
const res = await api.post<ApiResponse<ContractTask>>(`/contracts/${contractId}/tasks`, data);
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
|
// Allgemeine Aufgabe ohne Vertrag/Kunde (nur Mitarbeiter).
|
||||||
|
createGeneral: async (data: { title: string; description?: string }) => {
|
||||||
|
const res = await api.post<ApiResponse<ContractTask>>(`/tasks`, data);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
update: async (taskId: number, data: { title?: string; description?: string; visibleInPortal?: boolean }) => {
|
update: async (taskId: number, data: { title?: string; description?: string; visibleInPortal?: boolean }) => {
|
||||||
const res = await api.put<ApiResponse<ContractTask>>(`/tasks/${taskId}`, data);
|
const res = await api.put<ApiResponse<ContractTask>>(`/tasks/${taskId}`, data);
|
||||||
return res.data;
|
return res.data;
|
||||||
|
|||||||
@@ -471,7 +471,7 @@ export interface ContractTaskContract {
|
|||||||
|
|
||||||
export interface ContractTask {
|
export interface ContractTask {
|
||||||
id: number;
|
id: number;
|
||||||
contractId: number;
|
contractId: number | null;
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
status: ContractTaskStatus;
|
status: ContractTaskStatus;
|
||||||
|
|||||||
Reference in New Issue
Block a user