From 7e1d887b72b2343d0b44b6a00b259b2568a1d2e0 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Tue, 14 Jul 2026 15:39:45 +0330 Subject: [PATCH] feat(tags): match add-tag modal to tauri design (preset swatches + active toggle) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TagsSettingsPage modal: replace native color input with 4 preset swatches, add 'وضعیت برچسب' active toggle, update labels and submit button to mirror tauri AddPurchaseSubTabModal - TenantTagController::create now honors an optional 'active' flag (defaults true, non-breaking for existing callers) - tests: backend active-flag case, frontend preset-color + inactive case - docs/api/tag.md: document the new create 'active' field Co-Authored-By: Claude Opus 4.8 --- assets/admin/pages/TagsSettingsPage.test.tsx | 23 ++++++- assets/admin/pages/TagsSettingsPage.tsx | 65 +++++++++++++++----- docs/api/tag.md | 3 +- src/Tag/Controller/TenantTagController.php | 3 + tests/Tag/TenantTagTest.php | 18 ++++++ 5 files changed, 91 insertions(+), 21 deletions(-) diff --git a/assets/admin/pages/TagsSettingsPage.test.tsx b/assets/admin/pages/TagsSettingsPage.test.tsx index 47cf9db4..2f131258 100644 --- a/assets/admin/pages/TagsSettingsPage.test.tsx +++ b/assets/admin/pages/TagsSettingsPage.test.tsx @@ -36,9 +36,26 @@ describe('TagsSettingsPage', () => { await screen.findByText('فوری'); fireEvent.click(screen.getByRole('button', { name: /برچسب جدید/ })); - fireEvent.change(screen.getByPlaceholderText('مثلاً: فوری'), { target: { value: 'اورژانس' } }); - fireEvent.click(screen.getByRole('button', { name: 'ذخیره' })); + fireEvent.change(screen.getByPlaceholderText('نام برچسب را وارد کنید'), { target: { value: 'اورژانس' } }); + fireEvent.click(screen.getByRole('button', { name: 'ثبت برچسب' })); - await waitFor(() => expect(post).toHaveBeenCalledWith('/api/v1/tenant-tag', expect.objectContaining({ name: 'اورژانس' }))); + await waitFor(() => expect(post).toHaveBeenCalledWith('/api/v1/tenant-tag', expect.objectContaining({ + name: 'اورژانس', color: '#d61600', active: true, + }))); + }); + + it('sends the picked preset color and inactive status on create', async () => { + renderWithProviders(, { route: '/admin/tags-settings' }); + await screen.findByText('فوری'); + + fireEvent.click(screen.getByRole('button', { name: /برچسب جدید/ })); + fireEvent.change(screen.getByPlaceholderText('نام برچسب را وارد کنید'), { target: { value: 'بدهکار' } }); + fireEvent.click(screen.getByRole('button', { name: 'رنگ #0088ff' })); + fireEvent.click(screen.getByRole('checkbox', { name: 'وضعیت برچسب' })); // toggle off (default active) + fireEvent.click(screen.getByRole('button', { name: 'ثبت برچسب' })); + + await waitFor(() => expect(post).toHaveBeenCalledWith('/api/v1/tenant-tag', expect.objectContaining({ + name: 'بدهکار', color: '#0088ff', active: false, + }))); }); }); diff --git a/assets/admin/pages/TagsSettingsPage.tsx b/assets/admin/pages/TagsSettingsPage.tsx index f8a72a6f..99edaf89 100644 --- a/assets/admin/pages/TagsSettingsPage.tsx +++ b/assets/admin/pages/TagsSettingsPage.tsx @@ -14,11 +14,16 @@ import SettingsLayout from '../components/layout/SettingsLayout'; interface TenantTag { uuid: string; name: string; color: string; active: boolean } const schema = z.object({ - name: z.string().min(1, 'نام برچسب الزامی است'), - color: z.string().regex(/^#([0-9a-fA-F]{6})$/, 'رنگ نامعتبر است'), + name: z.string().min(1, 'نام برچسب الزامی است'), + color: z.string().regex(/^#([0-9a-fA-F]{6})$/, 'رنگ نامعتبر است'), + active: z.boolean(), }); type Form = z.infer; +/** Preset swatches — mirrors the tauri AddPurchaseSubTabModal palette. */ +const TAG_COLORS = ['#d61600', '#0088ff', '#ffa200', '#7cc985']; +const DEFAULTS: Form = { name: '', color: TAG_COLORS[0], active: true }; + const EMPTY: TenantTag[] = []; /** برچسب‌ها — per-tenant tag management inside the settings shell. */ @@ -33,13 +38,13 @@ export default function TagsSettingsPage() { }); const tags = data?.data ?? EMPTY; - const form = useForm
({ resolver: zodResolver(schema), defaultValues: { name: '', color: '#5559CE' } }); + const form = useForm({ resolver: zodResolver(schema), defaultValues: DEFAULTS }); const invalidate = () => qc.invalidateQueries({ queryKey: ['tenant-tags'] }); const createTag = useMutation({ mutationFn: (d: Form) => api.post('/api/v1/tenant-tag', d), - onSuccess: () => { invalidate(); setModal(null); form.reset({ name: '', color: '#5559CE' }); toast.success('برچسب ایجاد شد'); }, + onSuccess: () => { invalidate(); setModal(null); form.reset(DEFAULTS); toast.success('برچسب ایجاد شد'); }, onError: (e: any) => toast.error(e.message), }); const editTag = useMutation({ @@ -53,8 +58,8 @@ export default function TagsSettingsPage() { onError: (e: any) => { toast.error(e.message); setDeleteTarget(null); }, }); - const openCreate = () => { form.reset({ name: '', color: '#5559CE' }); setModal('create'); }; - const openEdit = (t: TenantTag) => { form.reset({ name: t.name, color: t.color }); setModal(t); }; + const openCreate = () => { form.reset(DEFAULTS); setModal('create'); }; + const openEdit = (t: TenantTag) => { form.reset({ name: t.name, color: t.color, active: t.active }); setModal(t); }; return ( @@ -89,27 +94,53 @@ export default function TagsSettingsPage() { )} - setModal(null)} title={modal === 'create' ? 'برچسب جدید' : 'ویرایش برچسب'}> + setModal(null)} title={modal === 'create' ? 'افزودن برچسب جدید' : 'ویرایش برچسب'}> { if (modal === 'create') createTag.mutate(d); else if (modal && typeof modal === 'object') editTag.mutate({ uuid: modal.uuid, d }); - })} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> + })} style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
- -
+ +
{form.formState.errors.name && {form.formState.errors.name.message}}
+
- -
- form.setValue('color', e.target.value)} style={{ width: 44, height: 38, border: '1px solid var(--border)', borderRadius: 'var(--r-sm)', background: 'none', cursor: 'pointer' }} /> - {form.watch('color')} + +
+ {TAG_COLORS.map((c) => { + const selected = form.watch('color') === c; + return ( +
-
- - + +
+ +
+ + {form.watch('active') ? 'فعال' : 'غیرفعال'} +
+ + diff --git a/docs/api/tag.md b/docs/api/tag.md index cb184c35..bb3c85bf 100644 --- a/docs/api/tag.md +++ b/docs/api/tag.md @@ -158,12 +158,13 @@ Without `sort`, the default ordering (weight/name) is unchanged. ### POST `/api/v1/tenant-tag` ```json -{ "name": "فوری", "color": "#FF0000" } +{ "name": "فوری", "color": "#FF0000", "active": true } ``` | Field | Type | Required | Validation | |-------|------|----------|------------| | `name` | string | ✅ | غیرخالی، حداکثر ۶۰ | | `color` | string | ❌ | هگز `#RRGGBB` یا `#RRGGBBAA` (پیش‌فرض `#5559CE`) | +| `active` | bool | ❌ | وضعیت اولیه (پیش‌فرض `true`) | Response `201`: TenantTag object. diff --git a/src/Tag/Controller/TenantTagController.php b/src/Tag/Controller/TenantTagController.php index af3599d7..f3f1f78d 100644 --- a/src/Tag/Controller/TenantTagController.php +++ b/src/Tag/Controller/TenantTagController.php @@ -68,6 +68,9 @@ class TenantTagController extends BaseController } $tag = new TenantTag($type, $id, $name, $color); + if (array_key_exists('active', $data)) { + $tag->setActive((bool) $data['active']); + } $this->tagRepo->save($tag); return $this->success($tag->toArray(), 201); diff --git a/tests/Tag/TenantTagTest.php b/tests/Tag/TenantTagTest.php index f8e02b95..1573fc80 100644 --- a/tests/Tag/TenantTagTest.php +++ b/tests/Tag/TenantTagTest.php @@ -51,6 +51,24 @@ class TenantTagTest extends ApiTestCase self::assertCount(0, $after['data']); } + public function testCreateHonorsActiveFlag(): void + { + [$user] = $this->doctorUser(); + + // explicit active:false is persisted, not forced to the default true + $created = $this->authJson('POST', '/api/v1/tenant-tag', $user, [ + 'name' => 'بایگانی', 'color' => '#123456', 'active' => false, + ]); + self::assertSame(201, $this->responseCode()); + self::assertFalse($created['data']['active']); + + // omitting active still defaults to true + $default = $this->authJson('POST', '/api/v1/tenant-tag', $user, [ + 'name' => 'پیش‌فرض', 'color' => '#654321', + ]); + self::assertTrue($default['data']['active']); + } + public function testRejectsInvalidNameAndColor(): void { [$user] = $this->doctorUser();