Lalabase
Tickets, Doku, Zeiterfassung und Kundengespräche an einem Ort, durchsuchbar per KI. Für Softwarefirmen mit Kundenprojekten. Open Source: auf deinem eigenen Server oder in unserer Cloud in Deutschland.
翻译准备中
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
The GEOTECH HR Portal is a leave, mission and permission request system for a
small-to-mid-size organization. Employees submit requests through a
role-based approval chain (Direct Manager → Technical Manager → General
Manager for Leave and Mission; Direct Manager only for Permission), HR
manages the employee directory, departments, leave types, public holidays
and approval routing, and attendance is recorded by HR and automatically
converted into permission-minute deductions.查看完整 README ↗The GEOTECH HR Portal is a leave, mission and permission request system for a small-to-mid-size organization. Employees submit requests through a role-based approval chain (Direct Manager → Technical Manager → General Manager for Leave and Mission; Direct Manager only for Permission), HR manages the employee directory, departments, leave types, public holidays and approval routing, and attendance is recorded by HR and automatically converted into permission-minute deductions.
The application is a single-page React/TypeScript frontend backed by a real Supabase project (Postgres, Auth, Row-Level Security, and SECURITY DEFINER RPCs). It is not a static demo — real authentication, real data, and real server-side authorization are live for the current production organization.
The application is feature-complete for V1 and is running against live Supabase data:
is_production) keeps
legacy/test accounts that must be retained for referential integrity out
of the normal HR Employees directory, without deleting them.The most recent stable checkpoint is commit b52a1e1 — "feat: finalize HR
production cleanup and controls" — committed locally, not pushed to any
remote.
The system has five roles, stored in profiles.role:
| Role | Summary |
|---|---|
| EMPLOYEE | Submits their own Leave/Mission/Permission requests, views their own request history, notifications, calendar, and profile. No approval authority. |
| MANAGER | Everything EMPLOYEE has, plus: approves requests from direct reports at the Direct Manager stage, views My Team (their direct reports). |
| TECH_MANAGER | Everything MANAGER has, plus: the singleton Technical Manager approval stage for every Leave/Mission chain in the company, and a read-only Organization directory. Exactly one active TECH_MANAGER can exist at a time. |
| GM | Everything TECH_MANAGER has (organization-wide view), plus: the singleton General Manager stage — always the final approval stage on any chain that includes one. Exactly one active GM can exist at a time. |
| HR | Employee directory (add/edit/activate/deactivate/assign manager & department/change password), Departments, Leave Types, Public Holidays, Approval Routing, company-wide Attendance entry, and a company-wide Requests view. HR also files requests for themselves like any other employee — HR is never an approval stage. |
Every role also has: My Requests (self-service submission history), Request Details, Notifications, My Calendar, and My Profile (including a self-service Change Password for real accounts).
profiles.manager_id (surfaced to the frontend as EmployeeProfile.managerId)
is the single source of truth for direct-manager relationships. There is
no separate hierarchy table — every "who reports to whom," "My Team,"
approval-chain, and authorization decision is derived directly from this one
column.
Current production hierarchy (5 active profiles):
HR Department (HR-001, HR)
|
Mona Mohamed Ahmed Hassan (MONAHASSAN, GM) ← no manager; top of the chain
|
Abd El Rahman Ahmed Soliman Hassan (ASOLIMAN, TECH_MANAGER)
|
Hassan Ahmed Hassan Ahmed (HASSANAHMED, MANAGER)
|
Yasmin Abdeljawad Zied Abdeljawad (YASMINZIED, EMPLOYEE)
HR's own manager_id is kept in sync with whoever holds the GM seat by the
set_approval_routing RPC (HR's Direct Manager is always the current
General Manager). Employee Code, name, role, and department for each of
these five are documented in §11; no passwords or secrets are recorded
anywhere in this repository.
manager_id
equals the signed-in user's own id — their direct reports only.profiles.manager_id; required for
every real employee before they can submit anything (Leave/Mission/
Permission submission is refused with "No active manager assigned" if it
is null or points at an inactive/wrong-role profile).My Team
membership check, and the singleton TM/GM promotion/demotion logic is
computed live from manager_id and company_approval_routing — never
cached or duplicated elsewhere.employee_id/manager_id/employee_name/manager_name at submission
time. A later reorganization (a manager reassignment, a routing change)
never rewrites a request already in flight or already decided — see §15.leave_types, HR-managed; e.g. Annual Leave, Emergency
Leave, Sick Leave, Maternity Leave, Paternity Leave, Unpaid Leave)count_working_days() counts every day in
the range that is not Friday/Saturday and not a public holiday.balance_key (ANNUAL or CASUAL)
are checked against leave_allocations; a pending request already
holds its working-day count against the remaining balance (so several
overlapping pending requests can't each look affordable), an approved
request consumes it, and a rejected request releases it. Leave types
with no balance_key (Sick/Maternity/Paternity/Unpaid) are not
balance-limited.EXCLUDE constraint
(requests_no_overlapping_leave) on the table itself, so no race
condition can ever create two overlapping PENDING/APPROVED leave requests
for the same employee.(end_date + end_time) > (start_date + start_time)), it does not need to be the same calendar day.CHECK constraint, so nothing that passes one can fail
the other.permission_minutes_per_month, 240 by default).Leave / Mission:
EMPLOYEE → DIRECT MANAGER → TECHNICAL MANAGER → GENERAL MANAGER → APPROVED
Permission:
EMPLOYEE → DIRECT MANAGER → APPROVED
The chain for Leave/Mission is built once, at submission, by
build_approval_chain(): Direct Manager, then the singleton Technical
Manager, then the singleton General Manager, skipping anyone who is
inactive, missing, or would appear a second time. It is snapshotted onto
the request's request_approvals rows — a later reorganization never
re-routes a request already in flight.
Documented, exact behavior:
request_approvals
row stores the approver's name at decision time, independent of any
later name/role change to that profile.submit_*_request RPC; the approval chain is built and snapshotted in
the same transaction; the first approver is notified.requests.status stays PENDING
until the final stage.requests.status = APPROVED, records decided_at/decided_by, and notifies the employee.delete_own_request() regardless of what the UI offers.Four notification types exist, all inserted server-side by the request RPCs (never fabricated client-side):
REQUEST_SUBMITTED — sent to an approver when a request reaches their
stage (including the very first stage at submission).REQUEST_ADVANCED — sent to the employee when their request moves to
the next stage.REQUEST_APPROVED — sent to the employee when a request is fully
approved.REQUEST_REJECTED — sent to the employee when a request is rejected.Each notification is read/unread (read boolean), can be marked read
individually or all-at-once, and clicking one navigates to the underlying
request. The header notification bell is global across every role and
routes to the correct role-specific Notifications page
(/employee/notifications, /manager/notifications, or
/hr/notifications), showing an unread-count badge.
Current V1 implementation:
/hr/attendance); it does not appear in the navigation for any other
role."My Calendar" (shown on the Employee dashboard and available to every role for their own record) renders the current month with one state per day, using the actual implemented precedence, checked in this order:
Annual Leave → Public Holiday → Present → Weekend → Normal
public_holidays.The calendar shows a legend for each state and rings today's date.
HR → Employees provides:
provision-employee Edge Function) and its matching profiles row in
one operation; the account can sign in immediately with the initial
password HR sets.profiles.role and
company_approval_routing in sync and respects the TM/GM singleton
constraint.departments table.hr-update-employee-password) that updates the target employee's real
Supabase Auth password server-side; HR is never shown or asked for the
employee's existing password.Production-directory rule: the normal Employees directory shows only
profiles where is_production = true. This is a boolean column on
profiles, evaluated by the frontend directory query — it is not
implemented as a hard-coded list of employee IDs, and it does not affect
Add Employee, Edit Employee, manager/team selectors, Approval Routing, or
any approval/authorization logic, all of which continue to see every
profile. See §16.
Full CRUD by HR: create a leave type with a name and an optional balance bucket (ANNUAL/CASUAL, or none for unlimited-by-policy types), edit its name/bucket, and activate/deactivate it. Only ACTIVE leave types are selectable when submitting a Leave request.
public_holidays_insert_hr,
public_holidays_update_hr, public_holidays_delete_hr, each gated on
is_hr()) and by the table grants themselves.Current production public-holiday data contains 13 records and is considered finalized at this checkpoint.
profiles (role = 'TECH_MANAGER' AND status = 'ACTIVE', role = 'GM' AND status = 'ACTIVE'), so at most
one active profile can hold each seat at any time.set_approval_routing(); it validates
the two candidates are active, distinct people, promotes them to the
right role, demotes whoever is fully vacating a seat back to MANAGER,
and keeps every HR profile's manager_id pointed at the current GM.HR can add a department and rename/archive an existing one (archiving sets
status = INACTIVE rather than deleting — no hard delete, so historical
employee/profile department references stay intact). The current
production environment contains 8 active departments: Human Resources,
Information Technology, Consulting Services Department, GIS & Spatial
Intelligence Department, Survey & Reality Capture Department, Remote
Sensing & Mapping Department, Data Management & Analytics Department, and
Product & Solutions Department.
Only functionality actually present in the current build is described above — no dashboard widget beyond what is listed here exists in V1.
SECURITY DEFINER RPC
(resolve_login_identity) resolves the entered code to that profile's
real Supabase Auth email — never a client-side guess — before
signInWithPassword is called.session.user.role.auth.uid(), is_hr(), or an
equivalent server-side check — never trusted from the client.submit_leave_request,
submit_mission_request, submit_permission_request, decide_request,
delete_own_request, hr_create_employee,
hr_create_manager_with_team, set_approval_routing, among others —
each independently re-verifies the caller's identity and authorization
rather than trusting the RLS policy alone.is_hr() inside the RPC/Edge Function
itself, not just by RLS or the UI.decide_request() checks the caller's
identity against the specific pending stage row before allowing an
APPROVE/REJECT.build_approval_chain()
and a database trigger, so no chain can be constructed or mutated to
place a stage after GM.protect_manager_with_reports).EXCLUDE constraint
backs the Leave overlap check described in §5, independent of
application-level validation.employee_id, manager_id,
employee_name, manager_name are captured on the request row at
submission time and never rewritten afterward.request_approvals row keeps its own
status, decided_at, and comment permanently once decided.approver_name is stored on the
approval row itself, so a later name or role change never alters what
the historical record shows.profiles.manager_id reflects the current
org chart; historical requests are unaffected by later changes to it,
since they carry their own snapshot fields instead of deriving from it.requests.archived_at / requests.archived_by
mark a request as retained-but-hidden. Archived requests are excluded
from every normal request list, dashboard, and count in the app (a
single shared fetch query filters archived_at IS NULL); archiving
never deletes or alters any of the request's own data, approval rows,
notifications, or audit log entries.is_production = true.is_production = false. These profiles cannot be safely deleted while
they are still referenced by protected historical requests, their
approval rows, or audit records (Postgres foreign keys on those tables
are RESTRICT, not cascading) — so they are retained, most deactivated,
rather than removed.true for every new profile created
going forward, so normal HR usage requires no extra step.This is a directory-visibility mechanism, not a security boundary.
is_production has no bearing on RLS, authentication, or authorization —
a non-production profile that is still ACTIVE is still a fully functional
account with whatever role/permissions its role column grants; the flag
only controls whether it appears in the normal HR Employees list.
GEOTECH-HR-Portal/
│
├── 01-DEMO/ standalone offline build (see its own docs)
├── 02-SOURCE/
│ ├── frontend/ the application source — run this
│ │ ├── src/app/App.tsx the whole UI: design system, shell, every page
│ │ ├── src/lib/store.ts data types, the StoreProvider, request/approval model
│ │ ├── src/lib/auth.ts Supabase Auth sign-in, session, HR password ops
│ │ ├── src/lib/supabase.ts the Supabase client (anon key only)
│ │ └── package.json
│ │
│ └── backend/ early prototype schema (5 migrations, Sept 1) —
│ superseded by supabase/migrations below; kept
│ for history, not the current source of truth.
│
├── 03-ACCOUNTS/ account/roster reference material
├── 04-DOCS/ specs, design guidelines, audit notes
├── 05-DESIGN/ Figma/design-token source
│
├── supabase/
│ ├── migrations/ the current, live database schema — 55 migrations,
│ │ applied to the linked Supabase project
│ └── functions/
│ ├── provision-employee/ creates a real Auth user + profile (HR-only)
│ └── hr-update-employee-password/ HR-authorized real password reset (HR-only)
│
└── README.md this file
supabase/migrations/ and supabase/functions/ are the current, active
backend — not 02-SOURCE/backend, which predates the real Supabase
integration.
The project is run locally from 02-SOURCE/frontend/:
cd 02-SOURCE/frontend
npm install
npm run dev # starts Vite on http://localhost:5173 (or the next free port)
Other scripts (from package.json):
npm run build # production build (vite build)
npm run preview # preview a production build locally
npm run typecheck # tsc --noEmit
A .env.local file (not committed) supplies VITE_SUPABASE_URL and
VITE_SUPABASE_ANON_KEY; without it the app falls back to local demo-mode
storage rather than talking to Supabase — see §19.
profiles.id is a foreign key straight into
auth.users.id.SET search_path = public) are how requests are submitted/decided/deleted and how HR
performs employee/routing/holiday/department management — see §14 for
the authorization model.provision-employee, hr-update-employee-password)
are the only places the service-role key is ever used, read from their
own server environment — never from a request body, never returned to
the caller, never present in any frontend file or build output.VITE_SUPABASE_ANON_KEY), which is safe to ship in a client bundle by
design — it has no authority beyond what RLS and the RPCs above grant it.This README does not include, and this repository should never contain, project secrets, service-role keys, passwords, JWT secrets, or other private credentials.
Major completed milestones, most recent first:
b52a1e1 (local checkpoint, not pushed).is_production.archived_at/archived_by), applied to the
two protected historical requests.DONE
is_production visibility)PARTIALLY COMPLETE / FINAL QA
is_production = false) cannot currently be fully removed — Postgres
RESTRICT foreign keys from the two protected historical requests block
their deletion, and the remaining ones are deactivated rather than
deleted.02-SOURCE/backend contains an early, superseded schema draft and
should not be used as a reference for the current database — see §17.provision-employee, hr-update-employee-password), read from their
own server-side environment; every privileged action they perform is
gated on independently re-verifying the caller is HR via their own JWT
first.