Docker-Images bauen und veröffentlichen / build (, omsorgCore/Dockerfile, omsorgcore) (push) Successful in 3s
Docker-Images bauen und veröffentlichen / build (, omsorgWeb/Dockerfile, omsorgweb) (push) Failing after 4s
Docker-Images bauen und veröffentlichen / build (VITE_OMSORG_CORE_URL=${{ vars.OMSORG_CORE_PUBLIC_URL }}, omsorgapp/Dockerfile, omsorgapp) (push) Successful in 3s
- omsorgapp: drop Electron, run as a plain Vite/React browser app; refresh token moves to an HttpOnly cookie (omsorgCore), CORS added for the new browser origin, document download/preview switched to Blob-based browser APIs. - Add Dockerfiles for omsorgCore, omsorgapp, and omsorgWeb, a docker-compose.yml wiring Postgres/MySQL/all three apps together, and a Gitea Actions workflow that builds and pushes images to the repo's container registry on push to main and on version tags. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
187 lines
5.5 KiB
React
187 lines
5.5 KiB
React
import { useState } from "react";
|
|
import { Pencil, Trash2 } from "lucide-react";
|
|
|
|
import OmsorgCard from "../../components/OmsorgCard";
|
|
import OmsorgButton from "../../components/ui/OmsorgButton";
|
|
import { useAuth } from "../../app/AuthContext";
|
|
import EditFacilityDialog from "./EditFacilityDialog";
|
|
import FacilityContactsList from "./FacilityContactsList";
|
|
import FacilityQualificationRatesList from "./FacilityQualificationRatesList";
|
|
|
|
function formatFollowUpDueDate(followUpDueDate) {
|
|
if (!followUpDueDate) {
|
|
return "—";
|
|
}
|
|
return new Date(followUpDueDate).toLocaleDateString("de-DE");
|
|
}
|
|
|
|
function formatAddress(street, postalCode, city, country) {
|
|
const line = [postalCode, city].filter(Boolean).join(" ");
|
|
const parts = [street, line].filter(Boolean);
|
|
|
|
if (country && country !== "Deutschland") {
|
|
parts.push(country);
|
|
}
|
|
|
|
return parts.length > 0 ? parts.join(", ") : "—";
|
|
}
|
|
|
|
export default function FacilityDetailPanel({ facility, onUpdated }) {
|
|
const { hasPermission } = useAuth();
|
|
const canEdit = hasPermission("Facilities", "Edit");
|
|
const canDelete = hasPermission("Facilities", "Delete");
|
|
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
|
|
async function handleDelete() {
|
|
if (!window.confirm(`${facility.name} wirklich löschen?`)) {
|
|
return;
|
|
}
|
|
|
|
setIsDeleting(true);
|
|
const result = await window.omsorg.facilities.delete(facility.id);
|
|
setIsDeleting(false);
|
|
|
|
if (result.ok) {
|
|
onUpdated?.(null);
|
|
}
|
|
}
|
|
|
|
if (!facility) {
|
|
return (
|
|
<OmsorgCard>
|
|
<div className="employee-detail-empty">
|
|
<h2>Keine Einrichtung ausgewählt</h2>
|
|
<p>Wähle links eine Einrichtung aus.</p>
|
|
</div>
|
|
</OmsorgCard>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<OmsorgCard>
|
|
<div className="employee-detail">
|
|
<div className="employee-detail-header">
|
|
<div>
|
|
<h2>{facility.name}</h2>
|
|
<span className="omsorg-badge omsorg-badge--neutral">{facility.crmStatus}</span>
|
|
</div>
|
|
|
|
{canEdit && (
|
|
<OmsorgButton variant="secondary" icon={Pencil} onClick={() => setIsEditDialogOpen(true)}>
|
|
Bearbeiten
|
|
</OmsorgButton>
|
|
)}
|
|
|
|
{canDelete && (
|
|
<OmsorgButton variant="secondary" icon={Trash2} onClick={handleDelete} disabled={isDeleting}>
|
|
Löschen
|
|
</OmsorgButton>
|
|
)}
|
|
</div>
|
|
|
|
<div className="employee-detail-grid">
|
|
<div>
|
|
<strong>Art der Einrichtung</strong>
|
|
<p>{facility.facilityType ?? "—"}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<strong>Website</strong>
|
|
<p>
|
|
{facility.website ? (
|
|
<a href={facility.website} target="_blank" rel="noreferrer">
|
|
{facility.website}
|
|
</a>
|
|
) : (
|
|
"—"
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<strong>Adresse</strong>
|
|
<p>{formatAddress(facility.street, facility.postalCode, facility.city, facility.country)}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<strong>Rechnungsadresse</strong>
|
|
<p>
|
|
{formatAddress(
|
|
facility.billingStreet,
|
|
facility.billingPostalCode,
|
|
facility.billingCity,
|
|
facility.billingCountry
|
|
)}
|
|
</p>
|
|
</div>
|
|
|
|
{facility.followUpDueDate && (
|
|
<div>
|
|
<strong>Wiedervorlage fällig am</strong>
|
|
<p>{formatFollowUpDueDate(facility.followUpDueDate)}</p>
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<strong>Verrechnungssatz</strong>
|
|
<p>{facility.billingRate != null ? `${facility.billingRate} EUR/Std.` : "—"}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<strong>Zuschläge</strong>
|
|
<p>
|
|
Nacht {facility.nightSurchargePercent ?? "—"}% · Sa {facility.saturdaySurchargePercent ?? "—"}% ·
|
|
So {facility.sundaySurchargePercent ?? "—"}% · Feiertag {facility.holidaySurchargePercent ?? "—"}%
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<strong>Fahrtkosten</strong>
|
|
<p>{facility.travelCostRate != null ? `${facility.travelCostRate} EUR (Pauschale je Einsatz)` : "—"}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<strong>Mindeststunden</strong>
|
|
<p>{facility.minimumHours ?? "—"}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<strong>Pausenregelung</strong>
|
|
<p>{facility.breakPolicy ?? "—"}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<strong>Abrechnungsintervall</strong>
|
|
<p>{facility.billingInterval ?? "—"}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<strong>Zahlungsziel</strong>
|
|
<p>{facility.paymentTermDays != null ? `${facility.paymentTermDays} Tage` : "—"}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<strong>Individuelle Vereinbarungen</strong>
|
|
<p>{facility.individualAgreements ?? "—"}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<FacilityQualificationRatesList facilityId={facility.id} />
|
|
<FacilityContactsList facilityId={facility.id} />
|
|
</div>
|
|
|
|
{isEditDialogOpen && (
|
|
<EditFacilityDialog
|
|
facility={facility}
|
|
onClose={() => setIsEditDialogOpen(false)}
|
|
onUpdated={(updatedFacility) => {
|
|
setIsEditDialogOpen(false);
|
|
onUpdated?.(updatedFacility);
|
|
}}
|
|
/>
|
|
)}
|
|
</OmsorgCard>
|
|
);
|
|
}
|