diff --git a/application/controllers/api/frontend/v1/MaZeitsperren.php b/application/controllers/api/frontend/v1/MaZeitsperren.php new file mode 100644 index 000000000..93c0f619f --- /dev/null +++ b/application/controllers/api/frontend/v1/MaZeitsperren.php @@ -0,0 +1,115 @@ + self::PERM_LOGGED, + 'getAllZeitsperrenFixeMa' => self::PERM_LOGGED, + 'getAllZeitsperrenLector' => self::PERM_LOGGED, + 'getAllZeitsperrenOes' => self::PERM_LOGGED, + 'getZeitsperrenAss' => self::PERM_LOGGED, + 'getStgLectors' => self::PERM_LOGGED, + 'loadZeitsperrenLectorStg' => self::PERM_LOGGED, + ]); + + // Load Libraries + $this->load->library('VariableLib', ['uid' => getAuthUID()]); + $this->load->library('form_validation'); + + // Load language phrases + $this->loadPhrases([ + 'ui', + 'person', + 'zeitsperren' + ]); + + // Load models + $this->load->model('ressource/Zeitsperre_model', 'ZeitsperreModel'); + } + + //only user with zeitsperren + public function getAllActiveZeitsperren($days) + { + + $result = $this->ZeitsperreModel->getZeitsperrenForNextDays($days); + $data = $this->getDataOrTerminateWithError($result); + + $this->terminateWithSuccess($data); + } + + //TODO: check query: more results after join with hr-table + //all fixe Ma + public function getAllZeitsperrenFixeMa($days) + { + $result = $this->ZeitsperreModel->getMitarbeiterWithZeitsperren($days, true, false, null, false); + $data = $this->getDataOrTerminateWithError($result); + + $this->terminateWithSuccess($data); + } + + public function getAllZeitsperrenLector($days) + { + $result = $this->ZeitsperreModel->getMitarbeiterWithZeitsperren($days, false, true, null, false); + + $data = $this->getDataOrTerminateWithError($result); + + $this->terminateWithSuccess($data); + } + + public function getZeitsperrenAss($days) + { + $result = $this->ZeitsperreModel->getMitarbeiterWithZeitsperren($days, false, false, null, true); + + $data = $this->getDataOrTerminateWithError($result); + + $this->terminateWithSuccess($data); + } + + public function getAllZeitsperrenOes($days, $oe) + { + $result = $this->ZeitsperreModel->getMitarbeiterWithZeitsperren($days, false, false, $oe, false); + + $data = $this->getDataOrTerminateWithError($result); + + $this->terminateWithSuccess($data); + } + + public function loadZeitsperrenLectorStg($days, $stg) + { + $result = $this->ZeitsperreModel->getMitarbeiterWithZeitsperren($days, false, true, false, false, $stg); + + $data = $this->getDataOrTerminateWithError($result); + + $this->terminateWithSuccess($data); + } + + public function getStgLectors() + { + $sql = " + select + studiengang_kz, + kurzbz, + kurzbzlang, + typ, + bezeichnung, + kurzbzlang || ' (' || bezeichnung || ')' AS label + from + public.tbl_studiengang + where + typ in ('b','m') + order by typ asc, kurzbz Asc + "; + + $this->load->model('organisation/Studiengang_model', 'StudiengangModel'); + $result = $this->StudiengangModel->execReadOnlyQuery($sql); + $data = $this->getDataOrTerminateWithError($result); + + $this->terminateWithSuccess($data); + } + +} + diff --git a/application/models/ressource/Zeitsperre_model.php b/application/models/ressource/Zeitsperre_model.php index 078d29d8b..4a6bc330e 100644 --- a/application/models/ressource/Zeitsperre_model.php +++ b/application/models/ressource/Zeitsperre_model.php @@ -61,4 +61,195 @@ class Zeitsperre_model extends DB_Model return $this->execQuery($qry); } + + /** + * getZeitsperren of Mitarbeiter for the next days + * @ days: count of the intervall of the next days + * @return array + */ + public function getZeitsperrenForNextDays($days, $type=null) + { + //TODO(Manu) von und bis im controller berechnen und hier übergeben + $von = date('Y-m-d'); + $bis = date('Y-m-d', strtotime("+{$days} days")); + + //version with campus.vw_mitarbeiter + $paramsArray = [$von, $bis, $bis, $von]; + $qry = "select + nachname, + vorname, + uid, + vondatum, + bisdatum, + vertretung_uid, + erreichbarkeit_kurzbz + --lektor, + -- fixangestellt + from + campus.vw_mitarbeiter + join campus.tbl_zeitsperre on + (uid = mitarbeiter_uid) + where + ((? <= bisdatum + and ? >= bisdatum) + or (?>= vondatum + and ? <= vondatum)) + "; + + //fix, lektor etc just with all ma + if($type=="fix") { + $qry.= "AND fixangestellt = true"; + } + if($type=="lector") { + $qry.= "AND lektor = true"; + } + + //wenn lektor= WAHR und fixangestellt = Falsch: --> externer Lektor + + $qry .= " order by nachname"; + + + + //version with hr.dienstverhaeltnis +/* $paramsArray = [$bis, $von]; + $qry = " + SELECT + ps.nachname, + ps.vorname, + zs.mitarbeiter_uid, + zs.vondatum, + zs.bisdatum, + zs.vertretung_uid, + zs.erreichbarkeit_kurzbz + FROM campus.tbl_zeitsperre zs + JOIN public.tbl_benutzer bn + ON bn.uid = zs.mitarbeiter_uid + JOIN public.tbl_person ps + ON ps.person_id = bn.person_id "; + + if ($type != null) { + $qry.= " JOIN hr.tbl_dienstverhaeltnis dv on bn.uid = dv.mitarbeiter_uid "; + } + + $qry.=" + WHERE + zs.vondatum <= ? + AND zs.bisdatum >= ?"; + + if($type=="fix") { + $qry.= " and dv.vertragsart_kurzbz = 'echterdv'"; + } + if($type=="lector") { + $qry.= " and dv.vertragsart_kurzbz in ('externerlehrender', 'gastlektor')"; + } + + $qry .= " + ORDER BY + ps.nachname, + ps.vorname, + zs.vondatum;";*/ + + $result = $this->execQuery($qry, $paramsArray); + + return $result; + } + + public function getMitarbeiterWithZeitsperren($days, $fix=false, $lector=false, $oe=null, $ass=false, $stg=false) + { + //TODO(Manu) von und bis im controller berechnen und hier übergeben + $von = date('Y-m-d'); + $bis = date('Y-m-d', strtotime("+{$days} days")); + + //version with campus.vw_mitarbeiter + $paramsArray = [$von, $bis, $bis, $von]; + + if($oe) + { + $paramsArray[] = $oe; + } + + if($stg) + { + $paramsArray[] = $stg; + } + + $qry = " + SELECT + m.nachname, + m.vorname, + m.uid, + z.vondatum, + z.bisdatum, + z.vertretung_uid, + z.erreichbarkeit_kurzbz, + m.lektor, + m.fixangestellt, + mv.kurzbz + FROM campus.vw_mitarbeiter m + join public.tbl_benutzer bn on bn.uid = m.uid"; + + if($oe || $ass || $stg) + { + $qry .= " JOIN public.tbl_benutzerfunktion bf on bf.uid = bn.uid"; + } + + if($stg) + { + $qry .= " JOIN public.tbl_studiengang stg using (oe_kurzbz)"; + } + + $qry.= " + LEFT JOIN campus.tbl_zeitsperre z + ON m.uid = z.mitarbeiter_uid + AND ( + (? <= z.bisdatum AND ? >= z.bisdatum) + OR + (? >= z.vondatum AND ? <= z.vondatum) + ) + left join public.tbl_mitarbeiter mv on z.vertretung_uid = mv.mitarbeiter_uid + where bn.aktiv = true + "; + + + if($fix) { + $qry.= " AND m.fixangestellt = true"; + } + if($lector) { + $qry.= " AND m.lektor = true"; + } + + if ($oe) + { + $qry.= " + AND bf.funktion_kurzbz ='oezuordnung' + AND bf.oe_kurzbz= ? + AND (bf.datum_von is null or bf.datum_von <=now()) + AND (bf.datum_bis is null or bf.datum_bis >= now()) + "; + } + + if ($ass) + { + $qry.= " + AND bf.funktion_kurzbz ='ass' + AND (bf.datum_von is null or bf.datum_von <=now()) + AND (bf.datum_bis is null or bf.datum_bis >= now()) + "; + } + + if ($stg) + { + $qry.= " + AND stg.studiengang_kz = ? + "; + } + + $qry .= " order by nachname, vorname, uid"; + + + $result = $this->execQuery($qry, $paramsArray); + + return $result; + + } } diff --git a/public/js/api/factory/zeitsperren.js b/public/js/api/factory/zeitsperren.js new file mode 100644 index 000000000..f6b9a9886 --- /dev/null +++ b/public/js/api/factory/zeitsperren.js @@ -0,0 +1,68 @@ +/** + * Copyright (C) 2026 fhcomplete.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +export default { + loadAllActiveZeitsperren(days) { + return { + method: 'get', + url:'api/frontend/v1/MaZeitsperren/getAllActiveZeitsperren/' + days + }; + }, + loadAllZeitsperrenFixeMa(days) { + return { + method: 'get', + url:'api/frontend/v1/MaZeitsperren/getAllZeitsperrenFixeMa/' + days + }; + }, + loadAllZeitsperrenLector(days) { + return { + method: 'get', + url:'api/frontend/v1/MaZeitsperren/getAllZeitsperrenLector/' + days + }; + }, + getAllOes(){ + return { + method: 'get', + url:'api/frontend/v1/funktionen/Funktionen/getAllOrgUnits' + }; + }, + loadAllZeitsperrenOE(days, oe) { + return { + method: 'get', + url:'api/frontend/v1/MaZeitsperren/getAllZeitsperrenOes/' + days + '/' + oe, + }; + }, + loadZeitsperrenAss(days) { + console.log("url: loadZeitsperrenAss"); + return { + method: 'get', + url:'api/frontend/v1/MaZeitsperren/getZeitsperrenAss/' + days + }; + }, + getAllStg(){ + return { + method: 'get', + url:'api/frontend/v1/MaZeitsperren/getStgLectors' + }; + }, + loadZeitsperrenLectorStg(days,stg) { + console.log("url: loadZeitsperrenLectorStg"); + return { + method: 'get', + url:'api/frontend/v1/MaZeitsperren/loadZeitsperrenLectorStg/' + days + '/' + stg, + }; + }, +} \ No newline at end of file diff --git a/public/js/apps/Cis/Cis.js b/public/js/apps/Cis/Cis.js index d4f846f25..e801f172e 100644 --- a/public/js/apps/Cis/Cis.js +++ b/public/js/apps/Cis/Cis.js @@ -19,6 +19,7 @@ import DeadlineOverview from "../../components/Cis/Abgabetool/DeadlineOverview.j import Studium from "../../components/Cis/Studium/Studium.js"; import StgOrgLvPlan from "../../components/Cis/LvPlan/StgOrg.js"; import OtherLvPlan from "../../components/Cis/LvPlan/OtherLvPlan.js"; +import ZeitsperrenMa from "../../components/Cis/ZeitsperrenMitarbeiter/ZeitsperrenMa.js"; import ApiRouteInfo from '../../api/factory/routeinfo.js'; import {capitalize} from "../../helpers/StringHelpers.js"; @@ -245,6 +246,12 @@ const router = VueRouter.createRouter({ }; }, }, + { + path: `/Cis/ZeitsperrenMa`, + name: 'ZeitsperrenMa', + component: ZeitsperrenMa, + props: true + }, ] }) diff --git a/public/js/components/Cis/ZeitsperrenMitarbeiter/Details/aktuelleZeitsperren.js b/public/js/components/Cis/ZeitsperrenMitarbeiter/Details/aktuelleZeitsperren.js new file mode 100644 index 000000000..65663184f --- /dev/null +++ b/public/js/components/Cis/ZeitsperrenMitarbeiter/Details/aktuelleZeitsperren.js @@ -0,0 +1,441 @@ +import {CoreFilterCmpt} from "../../../filter/Filter.js"; +import ApiMaTimelocks from "../../../../api/factory/zeitsperren.js"; +import FormForm from "../../../Form/Form.js"; +import FormInput from "../../../Form/Input.js"; +import PvAutoComplete from "../../../../../../index.ci.php/public/js/components/primevue/autocomplete/autocomplete.esm.min.js"; + +export default { + name: "aktuelleZeitsperren", + components: { + FormForm, + FormInput, + PvAutoComplete, + CoreFilterCmpt + }, + props: { + type: { + type: String, + default: 'all', + validator(value) { + return [ + 'fix', + 'lector', + 'oe', + 'ass', + 'lecStg' + ].includes(value) + } + } + }, + data(){ + return { + interval: 14, + days: {}, + arrayMaTimelocks: [], + listOes: [], + filteredOes: [], + selectedOe: null, + oe_kurzbz: null, + listStg: [], + filteredStg: [], + selectedStg: null, + studiengang_kz: null + } + }, + methods: { + loadAllActiveZeitsperren(days){ + this.$api + .call(ApiMaTimelocks.loadAllActiveZeitsperren(days)) + .then(result => { + this.arrayMaTimelocks = result.data; + }) + }, + loadZeitsperrenFixeMa(days){ + this.$api + .call(ApiMaTimelocks.loadAllZeitsperrenFixeMa(days)) + .then(result => { + this.arrayMaTimelocks = result.data; + }) + }, + loadZeitsperrenLector(days){ + this.$api + .call(ApiMaTimelocks.loadAllZeitsperrenLector(days)) + .then(result => { + this.arrayMaTimelocks = result.data; + }) + }, + loadZeitsperrenOE(days, oe){ + console.log("in function oe " + oe); + this.$api + .call(ApiMaTimelocks.loadAllZeitsperrenOE(days, oe)) + .then(result => { + this.arrayMaTimelocks = result.data; + }) + }, + loadZeitsperrenAss(days){ + console.log("in ASS "); + this.$api + .call(ApiMaTimelocks.loadZeitsperrenAss(days)) + .then(result => { + this.arrayMaTimelocks = result.data; + }) + }, + loadZeitsperrenStg(days, stg){ + console.log("in function stg " + stg); + this.$api + .call(ApiMaTimelocks.loadZeitsperrenLectorStg(days, stg)) + .then(result => { + this.arrayMaTimelocks = result.data; + }) + }, + createDays() { + const today = new Date(); + + //get Monday before today + const mondayBeforeToday = new Date(today); + const day = today.getDay(); + const diff = day === 0 ? 6 : day - 1; + + mondayBeforeToday.setDate(today.getDate() - diff); + + this.days = Array.from({ length: this.interval+1 }, (_, i) => { + const date = new Date(mondayBeforeToday); + date.setDate(mondayBeforeToday.getDate() + i); + + return { + date: date.toISOString().substring(0, 10), // 2026-07-13 + weekday: date.toLocaleDateString('de-AT', { weekday: 'short' }), + day: date.toLocaleDateString('de-AT', { + day: '2-digit', + month: '2-digit' + }) + }; + }); + }, + isBlocked(mitarbeiter, tag) { + if (!mitarbeiter?.sperren) { + return false; + } + + return mitarbeiter.sperren.some(s => + tag >= s.vondatum && + tag <= s.bisdatum + ); + }, + getSperre(mitarbeiter, tag){ + return mitarbeiter.sperren.find(s => + tag >= s.vondatum && + tag <= s.bisdatum + ); + }, + limitDays(event) { + let value = event.target.value.replace(/\D/g, '').slice(0, 2); + event.target.value = value; + this.interval = value ? Number(value) : null; + }, + filterOes(event) { + const query = event.query.toLowerCase(); + + this.filteredOes = this.listOes.filter(item => + item.label.toLowerCase().includes(query) + ); + }, + filterStg(event) { + const query = event.query.toLowerCase(); + + this.filteredStg = this.listStg.filter(item => + item.label.toLowerCase().includes(query) + ); + }, + //TODO(Manu) check link profile + link(uid) { + //https://www.fhcomplete.local/cis.php/Cis/Profil/getView/bell + return ( + FHC_JS_DATA_STORAGE_OBJECT.app_root + + FHC_JS_DATA_STORAGE_OBJECT.ci_router + + "/Cis/Profil/getView/" + + uid + ); + } + }, + computed: { + today(){ + const today = new Date(); + const day = today.toLocaleDateString('de-AT', { + day: '2-digit', + month: '2-digit', + year: 'numeric' + }); + return day; + }, + mondayBeforeToday(){ + const today = new Date(); + //get Monday before today + const mondayBeforeToday = new Date(today); + const day = today.getDay(); + const diff = day === 0 ? 6 : day - 1; + + mondayBeforeToday.setDate(today.getDate() - diff); + + return mondayBeforeToday; + }, + endInterval(){ + const today = new Date(); + const end = new Date(this.mondayBeforeToday); + end.setDate(end.getDate() + this.interval); + + return end.toLocaleDateString('de-AT', { + day: '2-digit', + month: '2-digit', + year: 'numeric' + }); + }, + mitarbeiter() { + const map = {}; + + //vor version with uid in query + this.arrayMaTimelocks.forEach(z => { + if (!map[z.uid]) { + map[z.uid] = []; + } + + map[z.uid].push(z); + }); + + return Object.entries(map).map(([mitarbeiter_uid, sperren]) => ({ + mitarbeiter_uid, + sperren + })); + }, + }, + watch: { + interval: { + handler(newVal) { + if (newVal) { + if(this.type == "fix") { + console.log("fix"); + this.loadZeitsperrenFixeMa(newVal); + } + else if(this.type == "lector") { + console.log("lector"); + this.loadZeitsperrenLector(newVal); + } + else if(this.type == "oe") { + console.log("oe: in handler"); + if(this.oe_kurzbz && this.oe_kurzbz !="") + this.loadZeitsperrenOE(newVal, this.oe_kurzbz); + else + console.log("Bitte OE auswählen"); + } + else if(this.type == "all") + { + console.log("all active"); + this.loadAllActiveZeitsperren(newVal); + } + else if(this.type == "ass") { + console.log("ass"); + this.loadZeitsperrenAss(newVal); + } + else + { + console.log("not defined"); + } + this.createDays(); + } + }, + deep: true, + }, + selectedOe(newVal) { + this.oe_kurzbz = newVal?.oe_kurzbz || ''; + }, + oe_kurzbz(newVal){ + { + if(newVal != "") + this.loadZeitsperrenOE(this.interval, newVal); + } + }, + selectedStg(newVal) { + this.studiengang_kz = newVal?.studiengang_kz || ''; + }, + studiengang_kz(newVal){ + { + console.log("STG triggered"); + if(newVal != "") + this.loadZeitsperrenStg(this.interval, newVal); + } + }, + }, + created(){ + if(this.type == "fix") { + console.log("fix"); + this.loadZeitsperrenFixeMa(this.interval); + } + else if(this.type == "lector") { + console.log("lector"); + this.loadZeitsperrenLector(this.interval); + } + else if(this.type == "oe") { + console.log("oeg"); + this.$api + .call(ApiMaTimelocks.getAllOes()) + .then(result => { + this.listOes = result.data; +/* this.listStg = this.listOes.filter(item => + item.label.includes("[Studiengang]") && item.aktiv || item.label.includes("[Lehrgang]") && item.aktiv + );*/ + }) + .catch(this.$fhcAlert.handleSystemError); + } + else if(this.type == "lecStg") { + console.log("lecStg"); + this.$api + .call(ApiMaTimelocks.getAllStg()) + .then(result => { + this.listStg = result.data; + }) + .catch(this.$fhcAlert.handleSystemError); + } + else if(this.type == "all") { + console.log("all active"); + this.loadAllActiveZeitsperren(this.interval); + } + else if(this.type == "ass") { + console.log("ass"); + this.loadZeitsperrenAss(this.interval); + } + else + { + console.log("not defined"); + } + this.createDays(); + +/* console.log('adams, 2026-07-16'); + console.log(this.isBlocked('adams', '2026-07-16'));*/ + }, + template: ` +
+

Mitarbeiter*innen mit aktuellen Zeitsperren + Fixangestellte + aller fixer Lektoren + nach Organisationseinheit {{ mondayBeforeToday.toLocaleDateString('de-AT') }} - {{endInterval}} +

+
+ + + + +
+ + + + + +
+ + +
+ + + + + +
+
+ +
count results: {{mitarbeiter.length}}, days: {{interval}} +
type: {{type}}
{{oe_kurzbz}} | {{selectedOe}} +
{{selectedStg}} | {{studiengang_kz}} + {{arrayMaTimelocks[0]}} +
+ + + + + + + + + + + + + + + + + +
UID +
{{ day.weekday }}
+
{{ day.day }}
+
{{m.sperren[0].nachname}} {{m.sperren[0].vorname}} + +
+ + + +
+ `, +} \ No newline at end of file diff --git a/public/js/components/Cis/ZeitsperrenMitarbeiter/ZeitsperrenMa.js b/public/js/components/Cis/ZeitsperrenMitarbeiter/ZeitsperrenMa.js new file mode 100644 index 000000000..3780cbfbe --- /dev/null +++ b/public/js/components/Cis/ZeitsperrenMitarbeiter/ZeitsperrenMa.js @@ -0,0 +1,116 @@ +import aktuelleZeitsperren from './Details/aktuelleZeitsperren.js'; + +export default { + name: 'ZeitsperrenMa', + components: { + aktuelleZeitsperren, + }, + data(){ + return { + arrayAllMa : [], + showAllCurrent: false, + showAllFix: false, + showAllLectors: false, + showSearchOes: false, + showAllAss: false, + showLectorsOe: false, + } + }, + methods: { + showTableAllCurrent(){ + this.showAllCurrent = !this.showAllCurrent; + this.showAllFix = false; + this.showAllLectors = false; + this.showSearchOes = false; + this.showAllAss = false; + this.showLectorsOe = false; + }, + showTableAllFix(){ + this.showAllFix = !this.showAllFix; + this.showAllCurrent = false; + this.showAllLectors = false; + this.showSearchOes = false; + this.showAllAss = false; + this.showLectorsOe = false; + }, + showTableAllLectors(){ + this.showAllLectors = !this.showAllLectors; + this.showAllFix = false; + this.showAllCurrent = false; + this.showSearchOes = false; + this.showAllAss = false; + this.showLectorsOe = false; + }, + showTableSearchOes(){ + this.showSearchOes = !this.showSearchOes; + this.showAllFix = false; + this.showAllCurrent = false; + this.showAllLectors = false; + this.showAllAss = false; + this.showLectorsOe = false; + }, + showTableAllAss(){ + this.showAllAss = !this.showAllAss; + this.showSearchOes = false; + this.showAllFix = false; + this.showAllCurrent = false; + this.showAllLectors = false; + this.showLectorsOe = false; + }, + showTableLectorsFromStg(){ + this.showLectorsOe = !this.showLectorsOe; + this.showSearchOes = false; + this.showAllFix = false; + this.showAllCurrent = false; + this.showAllAss = false; + this.showAllLectors = false; + }, + + }, + template: ` +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ + `, +} \ No newline at end of file diff --git a/public/js/components/Vertraege/Vertragsverwaltung.js b/public/js/components/Vertraege/Vertragsverwaltung.js index afde796b2..15277e28c 100644 --- a/public/js/components/Vertraege/Vertragsverwaltung.js +++ b/public/js/components/Vertraege/Vertragsverwaltung.js @@ -4,6 +4,7 @@ import FhcHeader from "../DetailHeader/DetailHeader.js"; import VertraegeMitarbeiter from "./Vertraege.js"; import VerticalSplit from "../verticalsplit/verticalsplit.js"; import ApiCoreVertraege from '../../api/factory/vertraege/vertraege.js'; +import ZeitsperrenProv from '../Cis/ZeitsperrenMitarbeiter/ZeitsperrenMa.js'; export default { @@ -13,7 +14,8 @@ export default { VerticalSplit, MitarbeiterHeader, FhcHeader, - VertraegeMitarbeiter + VertraegeMitarbeiter, + ZeitsperrenProv }, props: { config: Object, @@ -50,6 +52,9 @@ export default { }, template: ` + + +
diff --git a/public/vendor b/public/vendor new file mode 120000 index 000000000..9c39cc9f8 --- /dev/null +++ b/public/vendor @@ -0,0 +1 @@ +../vendor \ No newline at end of file