Migrate omsorgapp to browser SPA, add Docker/CI build setup
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>
This commit is contained in:
Felix Kemmler
2026-08-10 17:42:45 +02:00
co-authored by Claude Sonnet 5
parent e9e96a57dc
commit 598dfcd38a
461 changed files with 59753 additions and 2859 deletions
@@ -1,3 +1,5 @@
import { useEffect, useState } from "react";
import OmsorgButton from "../../components/ui/OmsorgButton";
import { useValueListItems } from "../../app/useValueListItems";
@@ -14,6 +16,17 @@ export const emptyFacilityForm = {
billingPostalCode: "",
billingCity: "",
billingCountry: "",
billingRate: "",
nightSurchargePercent: "",
saturdaySurchargePercent: "",
sundaySurchargePercent: "",
holidaySurchargePercent: "",
travelCostRate: "",
minimumHours: "",
breakPolicy: "",
billingInterval: "",
paymentTermDays: "",
individualAgreements: "",
};
export function facilityToFormValues(facility) {
@@ -30,10 +43,21 @@ export function facilityToFormValues(facility) {
billingPostalCode: facility.billingPostalCode ?? "",
billingCity: facility.billingCity ?? "",
billingCountry: facility.billingCountry ?? "",
billingRate: facility.billingRate ?? "",
nightSurchargePercent: facility.nightSurchargePercent ?? "",
saturdaySurchargePercent: facility.saturdaySurchargePercent ?? "",
sundaySurchargePercent: facility.sundaySurchargePercent ?? "",
holidaySurchargePercent: facility.holidaySurchargePercent ?? "",
travelCostRate: facility.travelCostRate ?? "",
minimumHours: facility.minimumHours ?? "",
breakPolicy: facility.breakPolicy ?? "",
billingInterval: facility.billingInterval ?? "",
paymentTermDays: facility.paymentTermDays ?? "",
individualAgreements: facility.individualAgreements ?? "",
};
}
export function facilityFormToPayload(form, { includeCrmStatus = false } = {}) {
export function facilityFormToPayload(form, { includeCrmStatus = false, followUpDays = null } = {}) {
const payload = {
name: form.name.trim(),
facilityType: form.facilityType || null,
@@ -50,14 +74,52 @@ export function facilityFormToPayload(form, { includeCrmStatus = false } = {}) {
if (includeCrmStatus) {
payload.crmStatus = form.crmStatus;
payload.followUpDays = followUpDays;
payload.billingRate = form.billingRate === "" ? null : Number(form.billingRate);
payload.nightSurchargePercent = form.nightSurchargePercent === "" ? null : Number(form.nightSurchargePercent);
payload.saturdaySurchargePercent = form.saturdaySurchargePercent === "" ? null : Number(form.saturdaySurchargePercent);
payload.sundaySurchargePercent = form.sundaySurchargePercent === "" ? null : Number(form.sundaySurchargePercent);
payload.holidaySurchargePercent = form.holidaySurchargePercent === "" ? null : Number(form.holidaySurchargePercent);
payload.travelCostRate = form.travelCostRate === "" ? null : Number(form.travelCostRate);
payload.minimumHours = form.minimumHours === "" ? null : Number(form.minimumHours);
payload.breakPolicy = form.breakPolicy.trim() || null;
payload.billingInterval = form.billingInterval || null;
payload.paymentTermDays = form.paymentTermDays === "" ? null : Number(form.paymentTermDays);
payload.individualAgreements = form.individualAgreements.trim() || null;
}
return payload;
}
export default function FacilityForm({ form, onChange, includeCrmStatus = false }) {
export default function FacilityForm({ form, onChange, includeCrmStatus = false, currentCrmStatus = null }) {
const { items: crmStatusOptions } = useValueListItems("CrmStatus");
const { items: facilityTypes } = useValueListItems("FacilityType");
const { items: billingIntervals } = useValueListItems("BillingInterval");
const [transitions, setTransitions] = useState([]);
useEffect(() => {
if (!includeCrmStatus) return;
let cancelled = false;
window.omsorg.valueLists.listTransitions("CrmStatus").then((result) => {
if (!cancelled) {
setTransitions(result.ok ? result.data ?? [] : []);
}
});
return () => {
cancelled = true;
};
}, [includeCrmStatus]);
const currentCrmStatusItem = crmStatusOptions.find((option) => option.value === currentCrmStatus);
const selectableCrmStatusOptions = currentCrmStatusItem
? crmStatusOptions.filter(
(option) =>
option.id === currentCrmStatusItem.id ||
transitions.some((t) => t.fromItemId === currentCrmStatusItem.id && t.toItemId === option.id)
)
: crmStatusOptions;
function updateField(field) {
return (event) => onChange({ ...form, [field]: event.target.value });
@@ -74,7 +136,7 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false
<label className="form-field">
<span>CRM-Status</span>
<select value={form.crmStatus} onChange={updateField("crmStatus")}>
{crmStatusOptions.map((option) => (
{selectableCrmStatusOptions.map((option) => (
<option key={option.id} value={option.value}>
{option.value}
</option>
@@ -151,6 +213,82 @@ export default function FacilityForm({ form, onChange, includeCrmStatus = false
<input value={form.billingCountry} onChange={updateField("billingCountry")} />
</label>
</fieldset>
{includeCrmStatus && (
<fieldset className="form-field form-field--fieldset">
<legend>Konditionen</legend>
<label className="form-field">
<span>Verrechnungssatz (EUR/Std.)</span>
<input type="number" min="0" step="0.01" value={form.billingRate} onChange={updateField("billingRate")} />
</label>
<div className="form-row">
<label className="form-field">
<span>Nachtzuschlag (%)</span>
<input type="number" min="0" step="0.1" value={form.nightSurchargePercent} onChange={updateField("nightSurchargePercent")} />
</label>
<label className="form-field">
<span>Samstagszuschlag (%)</span>
<input type="number" min="0" step="0.1" value={form.saturdaySurchargePercent} onChange={updateField("saturdaySurchargePercent")} />
</label>
</div>
<div className="form-row">
<label className="form-field">
<span>Sonntagszuschlag (%)</span>
<input type="number" min="0" step="0.1" value={form.sundaySurchargePercent} onChange={updateField("sundaySurchargePercent")} />
</label>
<label className="form-field">
<span>Feiertagszuschlag (%)</span>
<input type="number" min="0" step="0.1" value={form.holidaySurchargePercent} onChange={updateField("holidaySurchargePercent")} />
</label>
</div>
<div className="form-row">
<label className="form-field">
<span>Fahrtkosten (EUR, Pauschale je Einsatz)</span>
<input type="number" min="0" step="0.01" value={form.travelCostRate} onChange={updateField("travelCostRate")} />
</label>
<label className="form-field">
<span>Mindeststunden je Einsatz</span>
<input type="number" min="0" step="0.5" value={form.minimumHours} onChange={updateField("minimumHours")} />
</label>
</div>
<label className="form-field">
<span>Pausenregelung</span>
<input value={form.breakPolicy} onChange={updateField("breakPolicy")} />
</label>
<div className="form-row">
<label className="form-field">
<span>Abrechnungsintervall</span>
<select value={form.billingInterval} onChange={updateField("billingInterval")}>
<option value=""> nicht angegeben </option>
{billingIntervals.map((option) => (
<option key={option.id} value={option.value}>
{option.value}
</option>
))}
</select>
</label>
<label className="form-field">
<span>Zahlungsziel (Tage)</span>
<input type="number" min="0" step="1" value={form.paymentTermDays} onChange={updateField("paymentTermDays")} />
</label>
</div>
<label className="form-field">
<span>Individuelle Vereinbarungen</span>
<input value={form.individualAgreements} onChange={updateField("individualAgreements")} />
</label>
</fieldset>
)}
</div>
);
}