尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Book the crew. Run the job. Pay everyone the same day. AI agent for on-call crews: Bryntum Scheduler planning, availability links, hours reconciliation, PayPal Payouts. PayPal AI Hackathon 2026.
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗Book the crew. Run the job. Pay everyone the same day.
Shiftly is a workspace for a small operator who runs on-call crews: event stand builds, stage rigging, catering, removals. An AI agent turns a free-text brief into shifts, plans who works when on a Bryntum Scheduler, collects availability from freelancers on their phones, re-plans when someone declines, reconciles planned against actual hours, and computes pay. After the operator approves the total, it pays everyone through PayPal Payouts (Sandbox).
Built for the PayPal AI Hackathon 2026. MIT licensed.
| Step | Screen | Who acts |
|---|---|---|
| 1. Describe the job in plain words | /jobs/new | Operator writes, agent extracts dates, shifts, roles, client, budget |
| 2. Plan the crew | /jobs/[id] (Scheduler) | Agent proposes people; operator can drag shifts between people |
| 3. Send availability requests | Job page → Invitations | Operator sends; each person gets one signed link |
| 4. Accept or decline | /confirm/[token] (public, mobile) | Freelancer |
| 5. Rebalance a decline | Job page → Agent panel | Agent proposes a replacement; operator sends it |
| 6. Start the job, close shifts | /jobs/[id]/hours | Operator enters actual hours; agent flags differences over 15 min |
| 7. Pay the crew | Job page → Pay crew dialog | Operator confirms the total; PayPal Payouts sends it |
| 8. Track payouts | /payroll | PayPal reports item states; operator retries failures |
Job lifecycle: Planning → Crew confirmed → In progress → Done · to pay → Paid.
The agent uses OpenAI with structured JSON output (strict schemas) for three tasks:
src/lib/planning/constraints.ts, allocate.ts): role match, marked availability, no overlapping shifts, minimum rest between working days (11 h by default).If the AI service is down:
Rebalancing. When a freelancer declines, only that person's shifts are re-planned, after their answer is saved. The decliner is never offered the same shift again. The replacement is logged ("Luca declined Sat 14 Nov. Paolo takes over: …") and stays Proposed, not sent until the operator sends it.
Hours. Actual paid time is entered in 15-minute steps. Differences of more than 15 minutes (exactly 15 is fine) are flagged with a note. If the change pushes the day past the overtime threshold, the note says so. Approval needs every flag marked reviewed.
Pay. src/lib/pay.ts is pure and integer-only:
https://api-m.sandbox.paypal.com; no setting can point it at live money.SUCCESS item, and anyone whose money may still arrive: pending, on hold or unclaimed.ActivityLog row with an actor (operator, agent, paypal or crew), shown in the job's Agent panel.Payouts isn't covered by @paypal/paypal-server-sdk, so Shiftly calls the REST API with fetch (src/lib/paypal/client.ts). It uses OAuth client credentials with a cached token and a 20 s timeout. Errors fall into two kinds:
Pay crew (approvePayout)
payLines).planPayout decides what to do:
PayoutBatch with senderBatchId = job-{jobId}-{n} and one PayoutItem per person with senderItemId = job-{jobId}-worker-{workerId}-{n}. Then log payout.approved (operator).POST /v1/payments/payouts with header PayPal-Request-Id = senderBatchId. Store paypalBatchId, then fetch the batch to record per-item ids and states.Retry failed (retryFailed, from Payroll)
FAILED, at their approved amounts.retryOfId), so the failure stays visible in history.Unknown outcome
DRAFT and the UI says "We couldn't verify the payout response. Don't send again yet."PayPal-Request-Id and sender ids, so PayPal returns the original answer instead of paying twice.PayPal refused (validation, insufficient funds, credentials)
REJECTED with PayPal's message and "No money moved".Double clicks
senderBatchId makes the second insert fail, and that request reuses the first batch.PayPal webhook ──► POST /api/paypal/webhook ──► verify signature ──┐
├─► refreshBatch ─► GET /v1/payments/payouts/{id} ─► applyBatchStatus
cron / job page ─► POST /api/paypal/reconcile (x-cron-secret) ─────┘
POST /api/paypal/webhook:
POST /v1/notifications/verify-webhook-signature using PAYPAL_WEBHOOK_ID;PAYMENT.PAYOUTSBATCH.SUCCESS|DENIED|PROCESSING and PAYMENT.PAYOUTS-ITEM.SUCCEEDED|FAILED|BLOCKED|UNCLAIMED|RETURNED|REFUNDED|CANCELED;503 until PAYPAL_WEBHOOK_ID is set.POST /api/paypal/reconcile:
x-cron-secret: $SHIFTLY_CRON_SECRET;{ "jobId": 4 } or { "batchId": 2 } narrows it.applyBatchStatus (src/lib/payouts/service.ts):
SELECT … FOR UPDATE) and merges PayPal's snapshot through the pure mergeSnapshot;Done → Paid once every person with approved pay has a SUCCESS item.PayPal transaction_status | Shown as | Notes |
|---|---|---|
SUCCESS | Paid | The only state counted as paid |
NEW, PENDING, PROCESSING | Pending | Polled until final |
ONHOLD | On hold | PayPal review; not retried |
UNCLAIMED | Unclaimed | Recipient has no PayPal account yet. PayPal emails a sign-up link and returns the money after 30 days. Never re-sent |
FAILED | Failed | The only state Retry failed acts on |
RETURNED, REFUNDED, BLOCKED, CANCELED | Returned, Refunded, Blocked, Canceled | Final |
DRAFT (Shiftly) | Not sent | Saved, PayPal's answer not confirmed yet |
REJECTED (Shiftly) | Rejected | PayPal refused the request; no money moved |
A batch's label (Completed, Partially paid, Processing, Needs attention) is computed from its items, never from batch_status alone. Hovering a pill shows PayPal's raw status.
npm run payouts:smoke (scripts/payouts-smoke.ts) is the original Sandbox gate. It sends 3 × 1,00 € to SHIFTLY_PAYOUT_RECIPIENTS and polls until the batch settles.
@bryntum/scheduler@npm:@bryntum/scheduler-trial.next build past 8 GB of memory on Render.
scripts/vendor-bryntum.mjs runs before dev and build. It copies the prebuilt scheduler.module.js, scheduler.css, svalbard-light.css and the Font Awesome files into public/vendor/bryntum/ (gitignored).bryntum.ts loads them in the browser: the module through import(/* webpackIgnore: true */ url), the CSS as <link>s placed before the app's own styles. Only Bryntum's TypeScript types are imported at build time.scheduler-view.tsx creates a Scheduler once the module has loaded, then pushes data, zoom, height and the read-only state through Bryntum's runtime API (eventStore.data, viewPreset, setTimeSpan, …). The React wrapper isn't used, because it imports @bryntum/scheduler statically and would pull it back into the bundle. A skeleton shows while Bryntum loads; if it fails to load, the card says Couldn't load schedule. with a Retry button.timeRanges, plus a Now line.eventRenderer adds computed classes: role-installer|electrician|rigger|driver, status-proposed|sent|confirmed|declined, declined and conflict. The label shows the time window and role, plus a status icon.constrainDragToTimeSlot. Before the drop is final, the browser runs the same constraint checker as the server (checkAssignment) and rejects the move with the exact reason, e.g. "Giulia needs 11 hours of rest between working days (only 9h)". The server checks again before saving.Differences from docs/design/design-system.md §06:
Today button and no settings icon in the toolbar. Day chips (one per job day) jump to each day instead.Now marker uses Bryntum's built-in current-time line.The APIMatic Context Plugin for PayPal was installed during development (npx context-plugins install paypal, v0.4.1). It gives the coding agent PayPal SDK skills: setup, auth, endpoints, errors, models and testing for TypeScript, Python and .NET.
It was used to check what @paypal/paypal-server-sdk covers. Its TypeScript SDK map documents Orders but not Payouts, so following the brief, Payouts, OAuth and webhook verification are plain REST calls in src/lib/paypal/client.ts.
@prisma/adapter-pg on PostgreSQL (Render)public/vendor/bryntumOPENAI_MODELS: first = preferred, the rest are fallbacks)next/font); icons LucidetsxPrerequisites: Node 22+ and a PostgreSQL database (local, or the Render external URL).
cp .env.example .env.local # fill in the values (names below)
npm ci --ignore-scripts # see note on the Bryntum trial below
npx prisma generate
npm run db:deploy # apply migrations
npm run db:seed # demo company, crew and two jobs
npm run dev # http://localhost:3000
The Bryntum trial package ships an obfuscated postinstall script that the app doesn't need. Installing with --ignore-scripts skips it, which is also what the Render build does. Prisma's client is generated by npx prisma generate (and again by npm run build). npm run dev and npm run build first copy Bryntum's browser files into public/vendor/bryntum/.
Names only; see .env.example.
| Name | Used for |
|---|---|
DATABASE_URL | PostgreSQL connection string |
PAYPAL_CLIENT_ID, PAYPAL_SECRET | PayPal REST app credentials (Sandbox) |
PAYPAL_WEBHOOK_ID | Webhook signature verification; optional until the webhook is registered |
OPENAI_API_KEY | OpenAI API |
OPENAI_MODELS | Comma-separated model ids, first = preferred |
SHIFTLY_CRON_SECRET | x-cron-secret for /api/paypal/reconcile; also derives the invitation-link signing key |
SHIFTLY_PAYOUT_RECIPIENTS | Seed and smoke test only: 3 Sandbox Personal account emails |
| Script | What it does |
|---|---|
npm run dev / build / start | Next.js dev server, production build (runs prisma generate), production server |
npm test | Unit tests |
npm run lint / typecheck | ESLint, TypeScript |
npm run db:deploy | prisma migrate deploy |
npm run db:seed | Wipes all tables and seeds the demo |
npm run db:reset-demo | Applies pending migrations, then reseeds: back to the two seeded jobs; the next job created is J-0003 |
npm run payouts:smoke | Sandbox Payouts gate (3 × 1,00 €) |
The seed creates:
The three emails in SHIFTLY_PAYOUT_RECIPIENTS go to Elena Gallo, Sara Ricci and Paolo Ferrari. Everyone else has an @example.com address, so a demo payout shows real SUCCESS items next to UNCLAIMED ones. npm run db:reset-demo wipes everything else, including jobs created during a demo and their payout records.
npm test runs the unit tests in src/**/*.test.ts:
render.yaml defines one Node web service in Frankfurt:
npm ci --ignore-scripts && npx prisma migrate deploy && npm run buildnpm startnpm run db:reset-demo from a machine whose DATABASE_URL points at the Render database.On Render, set:
DATABASE_URL to the database's Internal URL. The app connects without TLS to private hostnames like dpg-…-a, and with verified TLS to external URLs.PAYPAL_CLIENT_ID, PAYPAL_SECRET, OPENAI_API_KEY, OPENAI_MODELS, SHIFTLY_CRON_SECRET.PAYPAL_WEBHOOK_ID once the webhook is registered at https://<service>.onrender.com/api/paypal/webhook.src/app/(operator)/ operator workspace: jobs, job detail, hours, crew, payroll, settings
src/app/confirm/[token]/ public, mobile invitation page
src/app/api/paypal/ webhook + reconcile routes
src/lib/planning/ constraints, allocation, planner (OpenAI + deterministic checks)
src/lib/payouts/ logic.ts (pure), service.ts (DB + PayPal), view.ts
src/lib/paypal/client.ts Sandbox REST client
src/lib/pay.ts, hours.ts pay and hours rules (pure)
prisma/ schema, migrations, seed
docs/ build brief and design handoff
These are deliberate scope cuts for the hackathon:
MIT, see LICENSE.
svalbard-light.css plus scheduler-theme.css, scoped to .shiftly-scheduler. It uses only Bryntum 7's documented CSS custom properties (--b-grid-*, --b-sch-event-*, --b-primary, …) and public classes, mapped to Shiftly's tokens.