Update staff roles, documentation, and add tenant API

Staff Roles:
- Remove Support Staff default role (now Manager and Staff only)
- Add position field for custom role ordering
- Update StaffRolesSettings with improved permission UI
- Add RolePermissions component for visual permission display

Documentation Updates:
- HelpStaff: Explain two-tier permission system (User Roles + Staff Roles)
- HelpSettingsStaffRoles: Update default roles, add settings access permissions
- HelpComprehensive: Update staff roles section with correct role structure
- HelpCustomers: Add customer creation and onboarding sections
- HelpContracts: Add lifecycle, snapshotting, and signing experience docs
- HelpSettingsAppearance: Update with 20 color palettes and navigation text

Tenant API:
- Add new isolated API at /tenant-api/v1/ for third-party integrations
- Token-based authentication with scope permissions
- Endpoints: business, services, resources, availability, bookings, customers, webhooks

Tests:
- Add test coverage for Celery tasks across modules
- Reorganize schedule view tests for better maintainability

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-24 20:46:36 -05:00
parent d8d3a4e846
commit 464726ee3e
47 changed files with 7826 additions and 2723 deletions

View File

@@ -71,7 +71,7 @@ const HelpApiOverview: React.FC = () => {
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4 mb-4">
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Base URL</h4>
<code className="text-sm text-brand-600 dark:text-brand-400">
https://your-subdomain.smoothschedule.com/api/v1/
https://your-subdomain.smoothschedule.com/tenant-api/v1/
</code>
</div>
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
@@ -79,15 +79,8 @@ const HelpApiOverview: React.FC = () => {
<Book size={18} className="text-blue-600 dark:text-blue-400 mt-0.5" />
<div>
<p className="text-sm text-blue-800 dark:text-blue-300">
<strong>Interactive Documentation:</strong> Explore and test API endpoints at{' '}
<a
href="/api/v1/docs/"
target="_blank"
rel="noopener noreferrer"
className="bg-blue-100 dark:bg-blue-900/40 px-1 rounded font-mono hover:underline"
>
/api/v1/docs/
</a>
<strong>Tenant Remote API:</strong> This API is designed for third-party integrations.
All requests require Bearer token authentication with appropriate scopes.
</p>
</div>
</div>
@@ -130,7 +123,7 @@ const HelpApiOverview: React.FC = () => {
<h4 className="font-medium text-gray-900 dark:text-white mb-3">Example Request</h4>
<div className="bg-gray-900 dark:bg-black rounded-lg p-4 overflow-x-auto">
<pre className="text-sm text-gray-100 font-mono">
{`curl -X GET "https://demo.smoothschedule.com/api/v1/services/" \\
{`curl -X GET "https://demo.smoothschedule.com/tenant-api/v1/services/" \\
-H "Authorization: Bearer ss_live_xxxxxxxxx" \\
-H "Content-Type: application/json"`}
</pre>
@@ -230,6 +223,216 @@ const HelpApiOverview: React.FC = () => {
</div>
</section>
{/* Filtering & Sorting Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<Code size={20} className="text-brand-500" />
Filtering & Sorting
</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<p className="text-gray-600 dark:text-gray-300 mb-4">
All list endpoints support filtering and sorting via query parameters.
</p>
{/* Comparison Operators */}
<div className="mb-6">
<h4 className="font-medium text-gray-900 dark:text-white mb-3">Comparison Operators</h4>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
For numeric fields, append these suffixes to filter by comparison:
</p>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-200 dark:border-gray-700">
<th className="text-left py-2 px-3 font-medium text-gray-900 dark:text-white">Suffix</th>
<th className="text-left py-2 px-3 font-medium text-gray-900 dark:text-white">Meaning</th>
<th className="text-left py-2 px-3 font-medium text-gray-900 dark:text-white">Example</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
<tr>
<td className="py-2 px-3"><code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">(none)</code></td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">Equals</td>
<td className="py-2 px-3"><code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">price=5000</code></td>
</tr>
<tr>
<td className="py-2 px-3"><code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">__lt</code></td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">Less than</td>
<td className="py-2 px-3"><code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">price__lt=5000</code></td>
</tr>
<tr>
<td className="py-2 px-3"><code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">__lte</code></td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">Less than or equal</td>
<td className="py-2 px-3"><code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">price__lte=5000</code></td>
</tr>
<tr>
<td className="py-2 px-3"><code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">__gt</code></td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">Greater than</td>
<td className="py-2 px-3"><code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">price__gt=5000</code></td>
</tr>
<tr>
<td className="py-2 px-3"><code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">__gte</code></td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">Greater than or equal</td>
<td className="py-2 px-3"><code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">price__gte=5000</code></td>
</tr>
</tbody>
</table>
</div>
</div>
{/* Sorting */}
<div className="mb-6">
<h4 className="font-medium text-gray-900 dark:text-white mb-3">Sorting</h4>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
Use the <code className="bg-gray-100 dark:bg-gray-700 px-1 rounded text-xs">ordering</code> parameter to sort results.
Prefix with <code className="bg-gray-100 dark:bg-gray-700 px-1 rounded text-xs">-</code> for descending order.
</p>
<div className="bg-gray-900 dark:bg-black rounded-lg p-4 overflow-x-auto">
<pre className="text-sm text-gray-100 font-mono">
{`# Ascending (oldest first)
GET /tenant-api/v1/bookings/?ordering=start_time
# Descending (newest first)
GET /tenant-api/v1/bookings/?ordering=-start_time`}
</pre>
</div>
</div>
{/* Services Filters */}
<div className="mb-6">
<h4 className="font-medium text-gray-900 dark:text-white mb-3">Services Endpoint Filters</h4>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-200 dark:border-gray-700">
<th className="text-left py-2 px-3 font-medium text-gray-900 dark:text-white">Parameter</th>
<th className="text-left py-2 px-3 font-medium text-gray-900 dark:text-white">Type</th>
<th className="text-left py-2 px-3 font-medium text-gray-900 dark:text-white">Description</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
<tr>
<td className="py-2 px-3"><code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">search</code></td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">string</td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">Search by name (partial match)</td>
</tr>
<tr>
<td className="py-2 px-3"><code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">name</code></td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">string</td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">Exact name match</td>
</tr>
<tr>
<td className="py-2 px-3"><code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">price</code></td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">integer</td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">Price in cents (supports __lt, __lte, __gt, __gte)</td>
</tr>
<tr>
<td className="py-2 px-3"><code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">duration</code></td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">integer</td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">Duration in minutes (supports __lt, __lte, __gt, __gte)</td>
</tr>
<tr>
<td className="py-2 px-3"><code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">variable_pricing</code></td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">boolean</td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">Filter by variable pricing (true/false)</td>
</tr>
<tr>
<td className="py-2 px-3"><code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">requires_manual_scheduling</code></td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">boolean</td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">Filter by manual scheduling requirement</td>
</tr>
<tr>
<td className="py-2 px-3"><code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">ordering</code></td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">string</td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">name, price, duration, display_order (prefix - for desc)</td>
</tr>
</tbody>
</table>
</div>
</div>
{/* Bookings Filters */}
<div className="mb-6">
<h4 className="font-medium text-gray-900 dark:text-white mb-3">Bookings Endpoint Filters</h4>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-200 dark:border-gray-700">
<th className="text-left py-2 px-3 font-medium text-gray-900 dark:text-white">Parameter</th>
<th className="text-left py-2 px-3 font-medium text-gray-900 dark:text-white">Type</th>
<th className="text-left py-2 px-3 font-medium text-gray-900 dark:text-white">Description</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
<tr>
<td className="py-2 px-3"><code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">start_date</code></td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">date</td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">Filter from date (YYYY-MM-DD)</td>
</tr>
<tr>
<td className="py-2 px-3"><code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">end_date</code></td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">date</td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">Filter to date (YYYY-MM-DD)</td>
</tr>
<tr>
<td className="py-2 px-3"><code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">start_datetime</code></td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">datetime</td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">Filter from datetime (ISO 8601)</td>
</tr>
<tr>
<td className="py-2 px-3"><code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">end_datetime</code></td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">datetime</td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">Filter to datetime (ISO 8601)</td>
</tr>
<tr>
<td className="py-2 px-3"><code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">status</code></td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">string</td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">SCHEDULED, CONFIRMED, COMPLETED, CANCELLED</td>
</tr>
<tr>
<td className="py-2 px-3"><code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">service_id</code></td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">integer</td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">Filter by service</td>
</tr>
<tr>
<td className="py-2 px-3"><code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">customer_id</code></td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">integer</td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">Filter by customer</td>
</tr>
<tr>
<td className="py-2 px-3"><code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">resource_id</code></td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">integer</td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">Filter by resource</td>
</tr>
<tr>
<td className="py-2 px-3"><code className="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">ordering</code></td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">string</td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">start_time, end_time, created_at, updated_at</td>
</tr>
</tbody>
</table>
</div>
</div>
{/* Example */}
<div>
<h4 className="font-medium text-gray-900 dark:text-white mb-3">Examples</h4>
<div className="bg-gray-900 dark:bg-black rounded-lg p-4 overflow-x-auto">
<pre className="text-sm text-gray-100 font-mono">
{`# Services under $50 with duration over 30 minutes
GET /tenant-api/v1/services/?price__lt=5000&duration__gt=30&ordering=price
# Bookings between two dates, newest first
GET /tenant-api/v1/bookings/?start_date=2025-01-01&end_date=2025-01-31&ordering=-start_time
# Customers search with sorting by last name
GET /tenant-api/v1/customers/?search=john&ordering=last_name`}
</pre>
</div>
</div>
</div>
</section>
{/* Rate Limiting Section */}
<section className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">