Replaces the template's static-data calendar with a multi-tenant calendar
backed by Appwrite calendar_events.
Schema/validation:
- lib/validation/calendar.ts (calendarEventSchema with cross-field check
end >= start)
- lib/appwrite/calendar-actions.ts: createCalendarEventAction,
updateCalendarEventAction, deleteCalendarEventAction. Date inputs
(HTML datetime-local 'YYYY-MM-DDTHH:mm', date 'YYYY-MM-DD') are
normalized to ISO 8601 before write.
- lib/appwrite/calendar-queries.ts: listCalendarEvents with optional
start/end range queries.
UI:
- /calendar server page: pulls events + customers, hands to CalendarClient.
- CalendarClient: month grid (6 rows × 7 cols), Monday-first, today badge,
prev/next/Bugün nav. Multi-day events show on every day in their range.
Each day cell shows up to 3 event chips with start time prefix; '+N
daha' for overflow. Hover reveals a + button to add an event on that day.
- EventFormSheet: title, all-day switch (toggles input type between
date and datetime-local), start/end with validation, customer FK,
color preset (blue/green/amber/red/violet/slate). Sentinel '__none__'
for nullable Selects. When editing, footer shows a destructive 'Sil'
ghost button on the left that triggers the parent's confirm dialog.
Color tokens centralized in COLOR_BG map; falls back to primary tint.
Removed all template calendar files (calendars.tsx, calendar-main, etc.)
since the data model didn't match.
Replaces the template's /tasks demo (deleted) with a real multi-tenant
Kanban board.
Schema/validation:
- lib/validation/tasks.ts (taskSchema with status/priority enums + dueDate
optional + assignee/customer optional)
- lib/appwrite/task-actions.ts: createTaskAction, updateTaskAction,
deleteTaskAction, moveTaskAction (used by drag-drop). All audit-logged;
moveTaskAction only audits when status actually changes.
- lib/appwrite/task-queries.ts: listTasks ordered by 'order' asc.
UI:
- /tasks server page assembles { tasks, customers, teamMembers } and
passes to TasksBoard. Removed the template's data-table demo files.
- TasksBoard (client): 4 droppable columns. Columns use @dnd-kit/core
useDroppable; cards inside each column are SortableContext+useSortable
for intra-column ordering. closestCorners collision detection.
- Drag-end computes new 'order' as midpoint between adjacent tasks
(no full reindex), updates UI optimistically, then persists via
moveTaskAction. Rolls back on server error with toast.
- TaskCard: priority badge (color-coded), due-date badge (red if
overdue), assignee badge, customer subtitle, dropdown (Edit/Delete)
on hover.
- TaskFormSheet: title/description/status/priority/dueDate/assignee/
customer. Uses sentinel '__none__' for nullable Selects (Radix Select
forbids empty string values), stripped before submit.
- DragOverlay shows the dragged card rotated 3deg with shadow.
Both files use client-only hooks (useTheme in sonner; useState/useMemo/
useId in chart via React) but the shadcn CLI shipped them without the
'use client' directive. Mounting <Toaster /> in the root server layout
crashed in production with:
Attempted to call useTheme() from the server but useTheme is on the
client.
Same risk on chart.tsx once any dashboard page renders <Chart>.
Verified all of src/components/ui/*.tsx — only these two needed the fix.
Future shadcn additions: check 'head -1' for the directive before
importing into server components.
Software catalog with per-customer assignments via the customer_software
join table. Two tabs in one /software page:
Catalog tab:
- Software CRUD: name, version, description, defaultFee (TRY).
- Deleting a software cascades and removes all its assignments first
(best-effort loop, then the catalog row), all wrapped in audit logs.
Assignments tab:
- M2M between customer and software with own fee (overrides defaultFee),
billingPeriod (monthly default), startDate/endDate, notes.
- Form auto-fills fee from selected software's defaultFee.
- Both Sheet forms localized; date inputs round-tripped via toIsoDate
(Appwrite expects ISO 8601 with TZ; HTML date input gives YYYY-MM-DD).
- Delete dialogs differentiated for catalog ('siliniyor') vs assignment
('kaldırılıyor').
New files:
- lib/validation/software.ts (softwareSchema + customerSoftwareSchema)
- lib/appwrite/software-actions.ts (6 server actions)
- lib/appwrite/software-queries.ts (listSoftware, listAssignments)
- lib/appwrite/software-types.ts (form state)
- /software route with SoftwareClient (Tabs), SoftwareFormSheet,
AssignmentFormSheet, inline delete dialogs.
Empty states surface the right next-step CTA: 'önce müşteri ekleyin', or
'önce yazılım ekleyin', as appropriate.
Same pattern as customers. Each service belongs to one customer (FK), has
unit price (TRY), billing period (monthly/yearly/onetime), and recurring
flag.
- lib/validation/services.ts: Zod schema with TRY price coercion (accepts
comma decimal), enum billing period, recurring boolean coercion.
- lib/appwrite/service-actions.ts: createServiceAction, updateServiceAction,
deleteServiceAction. Same tenant guard, audit log, team-scoped row perms.
- lib/appwrite/service-queries.ts: listServices, listServicesByCustomer.
- lib/format.ts: formatTRY (Intl.NumberFormat tr-TR), formatDate/DateTime,
BILLING_PERIOD_LABEL mapping (Aylık/Yıllık/Tek seferlik).
- /services page (server): joins services with customer names via
in-memory map.
- ServicesClient: TanStack table with global filter (name/customer/desc),
Repeat icon next to recurring badges, formatted TRY column.
- ServiceFormSheet: customer dropdown (disabled if no customers exist),
unit price + billing period + recurring switch.
- DeleteServiceDialog: same destructive confirmation pattern.
Empty state on /services CTA's user back to add a customer first if none
exist.
Establishes the multi-tenant module pattern. Subsequent modules (services,
software, calendar, tasks, finance, invoices) will copy this structure.
Validation:
- lib/validation/customers.ts: Zod schema with Turkish messages, optional
fields normalized to undefined.
Server actions (lib/appwrite/customer-actions.ts):
- createCustomerAction, updateCustomerAction, deleteCustomerAction
- All call requireTenant() guard, write team-scoped row permissions
(read+update by team, delete by owner|admin), and emit audit log.
- Update/delete cross-check tenantId on the existing row before mutating
(defense in depth even though row-level perms already enforce it).
- Field-level errors flattened from Zod for inline form display.
Server-side queries (lib/appwrite/customer-queries.ts):
- listCustomers(tenantId), getCustomer(tenantId, id) — admin SDK with
Query.equal('tenantId',...) tenant scope.
UI:
- /customers page (server component): pulls active tenant context, lists
customers, hands off to CustomersClient.
- CustomersClient: TanStack Table with global filter (name/email/phone/
taxId), column sorting on name + createdAt, pagination (20/page),
status badges, row actions (Edit/Delete dropdown), empty-state CTA.
- CustomerFormSheet: shadcn Sheet-based add/edit form with all fields,
toast feedback (sonner), inline field errors. Reused for create + update
by switching the action.
- DeleteCustomerDialog: confirmation modal with destructive button.
Infrastructure:
- Added sonner Toaster to root layout (richColors, closeButton).
- Updated metadata to 'İşletmem KovakCRM' and html lang='tr'.
- Renamed theme storage key to isletmem-ui-theme.
Multi-tenant invite system without SMTP dependency. Designed for dev/early
stage; promotes to email-driven later by adding SMTP to Appwrite.
New schema:
- invite_links table (code, email, role, status, expiresAt, invitedBy)
with unique index on code, indexes on (tenantId,status) and (tenantId,email)
New code:
- lib/appwrite/audit.ts: logAudit() helper writes to audit_logs with
X-Forwarded-For/User-Agent capture; never throws.
- lib/appwrite/tenant-guard.ts: requireTenant() returns
{ user, tenantId, role, settings }; pulls highest role from team
memberships. requireRole() guard.
- lib/appwrite/team-actions.ts:
* inviteMemberAction — creates short code (8 char nanoid-style),
inserts invite_links row with team-scoped perms, returns shortUrl.
Reuses existing pending invite for same email instead of duplicating.
Blocks self-invite, blocks invite of existing members.
* cancelInviteAction — owner/admin only, marks status=cancelled.
* removeMemberAction — owner/admin only; protects self-removal and
requires owner-on-owner.
* updateMemberRoleAction — owner only.
* resolveInviteCode — public-ish lookup by code (admin SDK).
* acceptInviteAction — verifies session.email matches invite.email,
creates membership via admin SDK, marks invite accepted.
All mutations write to audit_logs.
UI:
- /d/[code] short-URL accept page (server). Logged-in matching user
sees 'Daveti kabul et' button; non-matching user sees error; logged-out
user gets sign-up / sign-in CTAs that preserve the code.
- /settings/members page (server): InviteForm, PendingInvitesTable,
MembersTable. Owner/admin gates respected; only owner can change roles.
- Sign-up and sign-in forms accept ?invite=CODE (and ?email= for sign-up):
hidden input -> server action redirects to /d/CODE on success.
Other:
- next.config.ts: removed eslint config block (deprecated in Next 16);
kept typescript.ignoreBuildErrors for template legacy.
Template files (src/components/ui/chart.tsx, src/app/(dashboard)/tasks/
components/data-table-toolbar.tsx) carry pre-existing type errors that
block 'next build' but don't affect runtime. Our own code (lib/appwrite/*,
auth pages, dashboard) typechecks cleanly.
This unblocks Coolify deploys to isletmem.kovakcrm.com. Will revisit and
clean up the template files in a follow-up so we can re-enable strict
build-time checks.
- Layout split: (dashboard)/layout.tsx is now async server component that
fetches active context and passes user/company to (dashboard)/dashboard-shell.tsx
(client). Redirects to /onboarding if no tenant.
- AppSidebar:
* Header shows 'İşletmem' + the active company name (companyName from
tenant_settings), instead of mock 'ShadcnStore / Admin Dashboard'.
* Nav rebuilt for our modules in Turkish: Genel bakış, Müşteriler,
Hizmetler, Yazılımlarımız, Takvim, Görevler, Gelir/Gider, Faturalar,
Çalışma alanı (with submenu), Profil, Plan.
* Removed SidebarNotification (template promo widget).
* Accepts user/company props (typed via ShellUser/ShellCompany).
- NavUser:
* Real user name + email, no more 'ShadcnStore / store@example.com'.
* Avatar shows initials from name in primary/10 tinted square.
* Logout wired to signOutAction (server action) via useTransition.
* Menu items localized (Profil, Plan & Faturalama, Bildirimler, Çıkış yap).
- SiteHeader:
* Removed Blocks / Landing / GitHub external links (template demo links).
* Shows company name with Building2 icon between sidebar trigger and
search trigger.
* Search trigger moved to right side next to ModeToggle.
- Dropped UpgradeToProButton from the shell (template promo).
- Deleted dead-code src/components/layouts/base-layout.tsx (unused alt
layout that wasn't compatible with the new AppSidebar props).
- /dashboard now a server component:
* fetches active user + active tenant settings via getActiveContext()
* redirects to /onboarding if user has no tenant yet
* header shows companyName + 'Hoş geldiniz, {firstName}' + Turkish description
- Body reuses dashboard-2 components (Metrics/Sales/Revenue/Transactions/
TopProducts/CustomerInsights/QuickActions). Mock data for now; will be
swapped for live Appwrite queries as modules ship.
- New lib/appwrite/active-context.ts: getActiveContext() returns
{ user, tenantId, settings } for any server component / action.
- Made schema.ts SDK-agnostic by replacing Models.Document import with a
local SystemRow type ({ $id, $createdAt, $updatedAt, $permissions, ... }).
Avoids type clashes between appwrite (browser) and node-appwrite (server).
Two fixes triggered by user-reported error 'Invalid document structure:
Unknown attribute createdBy' during onboarding:
1) tenant_settings has no createdBy column by design (one row per tenant,
creator metadata is redundant). Removed createdBy from the row payload.
2) Made the action atomic: if any step after teams.create fails (row write,
prefs, cookie), delete the just-created team using the admin client.
Without this, two failed attempts left two orphan teams; reload then
redirected the user to /dashboard with no tenant_settings, trapping them.
Already cleaned up the two orphan teams via Appwrite MCP.
- /onboarding page (server component): redirects to /sign-in if not authed,
to /dashboard if user already has a team. Otherwise renders form.
- createWorkspaceAction:
* teams.create (user becomes owner via session SDK)
* tablesDB.createRow on tenant_settings (admin SDK) with team-scoped permissions:
Permission.read(Role.team(id)), update(owner|admin), delete(owner)
* account.updatePrefs({ activeTenant }) — persisted source of truth
* isletmem-tenant cookie for fast access
* redirect to /dashboard
- setActiveTenantAction stub for future workspace switcher
- lib/appwrite/tenant.ts: getUserTeams, getActiveTenantId helpers (server-only)
- tenant-types.ts holds WorkspaceState + ACTIVE_TENANT_COOKIE (no 'use server')
node-appwrite 24.x and appwrite 25.x target Appwrite server 1.9.1.
Our self-hosted server runs 1.9.0, which produced an SDK-version
mismatch warning on every API call.
Pinned:
node-appwrite: ^23.1.0 (was ^24.0.0)
appwrite: ^24.2.0 (was ^25.0.0)
v23/v24 keep both positional and params-object overloads, so existing
auth-actions.ts calls (createEmailPasswordSession, createRecovery,
account.create) compile and run unchanged. When we upgrade Appwrite to
1.9.1 we can bump the SDKs back.
Server-action files ('use server') can only export async functions.
Exporting initialAuthState (object) caused:
'A use server file can only export async functions, found object'
when sign-up form was submitted.
Moved AuthState type and initialAuthState const to lib/appwrite/auth-types.ts.
Updated 3 form components to import the const from the new location.
- Server actions in lib/appwrite/auth-actions.ts:
signInAction, signUpAction, forgotPasswordAction, signOutAction
All use node-appwrite admin client; session secret stored as httpOnly
cookie (isletmem-session). Errors localized to Turkish.
- Redesigned /sign-in and /sign-up using sign-in-3 split-card layout,
branded as 'İşletmem' with gradient brand panel (no external image).
Removed social login buttons (email/password only for now).
- /forgot-password localized; success state shows email-sent confirmation.
- Auth pages redirect to /dashboard if user already has a session.
- middleware.ts:
* Protects /dashboard, /onboarding, /settings — redirects to /sign-in?redirect=...
* Auth pages redirect logged-in users to /dashboard
* Keeps legacy /login and /register redirects