2026-10-07 12:29:23 +03:30
|
|
|
|
<script setup lang="ts">
|
2026-11-03 13:15:21 +03:30
|
|
|
|
import { computed, ref, watch } from 'vue';
|
2026-10-07 12:29:23 +03:30
|
|
|
|
import type {
|
|
|
|
|
|
ActivityChecklistItem,
|
|
|
|
|
|
ActivityFieldDef,
|
|
|
|
|
|
ActivityFieldValue,
|
|
|
|
|
|
ActivityFieldValues,
|
|
|
|
|
|
ActivityParticipantsValue,
|
|
|
|
|
|
OrgUserDto,
|
|
|
|
|
|
PaginatedResponse,
|
|
|
|
|
|
PersonDto,
|
|
|
|
|
|
} from '@mento/shared';
|
|
|
|
|
|
import { api } from '@/lib/api';
|
2026-11-03 13:15:21 +03:30
|
|
|
|
import { canAssignToOthers } from '@/lib/assign-access';
|
|
|
|
|
|
import { useAuthStore } from '@/stores/auth';
|
2026-10-07 12:29:23 +03:30
|
|
|
|
import { toFaDigits } from '@/lib/jalali';
|
|
|
|
|
|
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
|
|
|
|
|
import UserMultiSelect from '@/components/UserMultiSelect.vue';
|
|
|
|
|
|
import UiIcon from '@/components/UiIcon.vue';
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
|
fields: ActivityFieldDef[];
|
|
|
|
|
|
users: OrgUserDto[];
|
|
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
|
|
const model = defineModel<ActivityFieldValues>({ required: true });
|
2026-11-03 13:15:21 +03:30
|
|
|
|
const auth = useAuthStore();
|
|
|
|
|
|
const canAssignOthers = computed(() => canAssignToOthers(auth.user, 'activities'));
|
2026-10-07 12:29:23 +03:30
|
|
|
|
|
|
|
|
|
|
function get<T extends ActivityFieldValue>(key: string, fallback: T): T {
|
|
|
|
|
|
const v = model.value[key];
|
|
|
|
|
|
return (v ?? fallback) as T;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function set(key: string, value: ActivityFieldValue) {
|
|
|
|
|
|
model.value = { ...model.value, [key]: value };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function str(key: string): string {
|
|
|
|
|
|
const v = model.value[key];
|
|
|
|
|
|
return typeof v === 'string' || typeof v === 'number' ? String(v) : '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function multi(key: string): string[] {
|
|
|
|
|
|
const v = model.value[key];
|
|
|
|
|
|
return Array.isArray(v) && v.every((x) => typeof x === 'string') ? (v as string[]) : [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggleMulti(key: string, opt: string) {
|
|
|
|
|
|
const list = multi(key);
|
|
|
|
|
|
set(key, list.includes(opt) ? list.filter((x) => x !== opt) : [...list, opt]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ---------- چکلیست ---------- */
|
|
|
|
|
|
const checklistDraft = ref<Record<string, string>>({});
|
|
|
|
|
|
|
|
|
|
|
|
function checklist(key: string): ActivityChecklistItem[] {
|
|
|
|
|
|
const v = model.value[key];
|
|
|
|
|
|
return Array.isArray(v) ? (v as ActivityChecklistItem[]).filter((x) => x && typeof x === 'object') : [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function addChecklistItem(key: string) {
|
|
|
|
|
|
const text = (checklistDraft.value[key] ?? '').trim();
|
|
|
|
|
|
if (!text) return;
|
|
|
|
|
|
const id = `c_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
|
|
|
|
set(key, [...checklist(key), { id, text, done: false }]);
|
|
|
|
|
|
checklistDraft.value = { ...checklistDraft.value, [key]: '' };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateChecklistItem(key: string, id: string, patch: Partial<ActivityChecklistItem>) {
|
|
|
|
|
|
set(
|
|
|
|
|
|
key,
|
|
|
|
|
|
checklist(key).map((it) => (it.id === id ? { ...it, ...patch } : it)),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function removeChecklistItem(key: string, id: string) {
|
|
|
|
|
|
set(
|
|
|
|
|
|
key,
|
|
|
|
|
|
checklist(key).filter((it) => it.id !== id),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function moveChecklistItem(key: string, index: number, dir: -1 | 1) {
|
|
|
|
|
|
const list = [...checklist(key)];
|
|
|
|
|
|
const to = index + dir;
|
|
|
|
|
|
if (to < 0 || to >= list.length) return;
|
|
|
|
|
|
const [it] = list.splice(index, 1);
|
|
|
|
|
|
list.splice(to, 0, it!);
|
|
|
|
|
|
set(key, list);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ---------- شرکتکنندگان ---------- */
|
|
|
|
|
|
function participants(key: string): ActivityParticipantsValue {
|
|
|
|
|
|
const v = model.value[key] as ActivityParticipantsValue | null | undefined;
|
|
|
|
|
|
return {
|
|
|
|
|
|
userIds: v?.userIds ?? [],
|
|
|
|
|
|
people: v?.people ?? [],
|
|
|
|
|
|
guests: v?.guests ?? [],
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function patchParticipants(key: string, patch: Partial<ActivityParticipantsValue>) {
|
|
|
|
|
|
set(key, { ...participants(key), ...patch });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const guestDraft = ref<Record<string, string>>({});
|
|
|
|
|
|
function addGuest(key: string) {
|
|
|
|
|
|
const name = (guestDraft.value[key] ?? '').trim();
|
|
|
|
|
|
if (!name) return;
|
|
|
|
|
|
const p = participants(key);
|
|
|
|
|
|
if (!p.guests.includes(name)) patchParticipants(key, { guests: [...p.guests, name] });
|
|
|
|
|
|
guestDraft.value = { ...guestDraft.value, [key]: '' };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const personQuery = ref<Record<string, string>>({});
|
|
|
|
|
|
const personResults = ref<Record<string, PersonDto[]>>({});
|
|
|
|
|
|
const personOpenKey = ref('');
|
|
|
|
|
|
let personTimer: ReturnType<typeof setTimeout> | undefined;
|
|
|
|
|
|
let personSeq = 0;
|
|
|
|
|
|
|
|
|
|
|
|
function searchPeople(key: string) {
|
|
|
|
|
|
personOpenKey.value = key;
|
|
|
|
|
|
if (personTimer) clearTimeout(personTimer);
|
|
|
|
|
|
const q = (personQuery.value[key] ?? '').trim();
|
|
|
|
|
|
if (!q) {
|
|
|
|
|
|
personResults.value = { ...personResults.value, [key]: [] };
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const seq = ++personSeq;
|
|
|
|
|
|
personTimer = setTimeout(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await api.get<PaginatedResponse<PersonDto>>('/people', {
|
|
|
|
|
|
params: { q, limit: 8 },
|
|
|
|
|
|
});
|
|
|
|
|
|
if (seq === personSeq) personResults.value = { ...personResults.value, [key]: data.items };
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
if (seq === personSeq) personResults.value = { ...personResults.value, [key]: [] };
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 280);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function addPerson(key: string, p: PersonDto) {
|
|
|
|
|
|
const cur = participants(key);
|
|
|
|
|
|
if (!cur.people.some((x) => x.id === p.id)) {
|
|
|
|
|
|
patchParticipants(key, { people: [...cur.people, { id: p.id, name: p.fullName }] });
|
|
|
|
|
|
}
|
|
|
|
|
|
personQuery.value = { ...personQuery.value, [key]: '' };
|
|
|
|
|
|
personResults.value = { ...personResults.value, [key]: [] };
|
|
|
|
|
|
personOpenKey.value = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => props.fields.map((f) => f.key).join('|'),
|
|
|
|
|
|
() => {
|
|
|
|
|
|
checklistDraft.value = {};
|
|
|
|
|
|
guestDraft.value = {};
|
|
|
|
|
|
personQuery.value = {};
|
|
|
|
|
|
personResults.value = {};
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="fields-grid">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="f in fields"
|
|
|
|
|
|
:key="f.key"
|
|
|
|
|
|
class="cf"
|
|
|
|
|
|
:class="{ wide: ['textarea', 'checklist', 'participants', 'multiselect'].includes(f.type) }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<label :for="`cf-${f.key}`">
|
|
|
|
|
|
{{ f.label }}
|
|
|
|
|
|
<i v-if="f.required">*</i>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-if="f.type === 'text' || f.type === 'url'"
|
|
|
|
|
|
:id="`cf-${f.key}`"
|
|
|
|
|
|
:type="f.type === 'url' ? 'url' : 'text'"
|
|
|
|
|
|
:dir="f.type === 'url' ? 'ltr' : undefined"
|
|
|
|
|
|
:value="str(f.key)"
|
|
|
|
|
|
:placeholder="f.placeholder || ''"
|
|
|
|
|
|
maxlength="1000"
|
|
|
|
|
|
@input="set(f.key, ($event.target as HTMLInputElement).value)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
v-else-if="f.type === 'textarea'"
|
|
|
|
|
|
:id="`cf-${f.key}`"
|
|
|
|
|
|
:value="str(f.key)"
|
|
|
|
|
|
:placeholder="f.placeholder || ''"
|
|
|
|
|
|
rows="3"
|
|
|
|
|
|
@input="set(f.key, ($event.target as HTMLTextAreaElement).value)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-else-if="f.type === 'number'"
|
|
|
|
|
|
:id="`cf-${f.key}`"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
inputmode="decimal"
|
|
|
|
|
|
dir="ltr"
|
|
|
|
|
|
:value="str(f.key)"
|
|
|
|
|
|
:placeholder="f.placeholder || ''"
|
|
|
|
|
|
@input="
|
|
|
|
|
|
set(
|
|
|
|
|
|
f.key,
|
|
|
|
|
|
($event.target as HTMLInputElement).value === ''
|
|
|
|
|
|
? null
|
|
|
|
|
|
: Number(($event.target as HTMLInputElement).value),
|
|
|
|
|
|
)
|
|
|
|
|
|
"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<JalaliDateInput
|
|
|
|
|
|
v-else-if="f.type === 'date'"
|
|
|
|
|
|
:id="`cf-${f.key}`"
|
|
|
|
|
|
:model-value="str(f.key)"
|
|
|
|
|
|
@update:model-value="(v: string) => set(f.key, v || null)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<select
|
|
|
|
|
|
v-else-if="f.type === 'select'"
|
|
|
|
|
|
:id="`cf-${f.key}`"
|
|
|
|
|
|
:value="str(f.key)"
|
|
|
|
|
|
@change="set(f.key, ($event.target as HTMLSelectElement).value || null)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="">— انتخاب کنید —</option>
|
|
|
|
|
|
<option v-for="opt in f.options ?? []" :key="opt" :value="opt">{{ opt }}</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else-if="f.type === 'multiselect'" class="chips">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="opt in f.options ?? []"
|
|
|
|
|
|
:key="opt"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="chip-opt"
|
|
|
|
|
|
:class="{ on: multi(f.key).includes(opt) }"
|
|
|
|
|
|
@click="toggleMulti(f.key, opt)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<UiIcon v-if="multi(f.key).includes(opt)" name="check-plain" :size="12" />
|
|
|
|
|
|
{{ opt }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<label v-else-if="f.type === 'checkbox'" class="switch">
|
|
|
|
|
|
<input
|
|
|
|
|
|
:id="`cf-${f.key}`"
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
:checked="get<boolean>(f.key, false) === true"
|
|
|
|
|
|
@change="set(f.key, ($event.target as HTMLInputElement).checked)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<i />
|
|
|
|
|
|
<span>{{ get<boolean>(f.key, false) === true ? 'بله' : 'خیر' }}</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else-if="f.type === 'checklist'" class="checklist">
|
|
|
|
|
|
<p v-if="checklist(f.key).length" class="progress">
|
|
|
|
|
|
{{ toFaDigits(checklist(f.key).filter((x) => x.done).length) }} از
|
|
|
|
|
|
{{ toFaDigits(checklist(f.key).length) }} انجام شده
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<ul>
|
|
|
|
|
|
<li v-for="(it, idx) in checklist(f.key)" :key="it.id" :class="{ done: it.done }">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
:checked="it.done"
|
|
|
|
|
|
aria-label="انجام شد"
|
|
|
|
|
|
@change="updateChecklistItem(f.key, it.id, { done: ($event.target as HTMLInputElement).checked })"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<input
|
|
|
|
|
|
class="item-text"
|
|
|
|
|
|
:value="it.text"
|
|
|
|
|
|
maxlength="300"
|
|
|
|
|
|
@input="updateChecklistItem(f.key, it.id, { text: ($event.target as HTMLInputElement).value })"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<button type="button" class="mini" :disabled="idx === 0" aria-label="بالا" @click="moveChecklistItem(f.key, idx, -1)">▲</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="mini"
|
|
|
|
|
|
:disabled="idx === checklist(f.key).length - 1"
|
|
|
|
|
|
aria-label="پایین"
|
|
|
|
|
|
@click="moveChecklistItem(f.key, idx, 1)"
|
|
|
|
|
|
>
|
|
|
|
|
|
▼
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" class="mini danger" aria-label="حذف" @click="removeChecklistItem(f.key, it.id)">
|
|
|
|
|
|
<UiIcon name="x" :size="13" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
<div class="add-row">
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="checklistDraft[f.key]"
|
|
|
|
|
|
maxlength="300"
|
|
|
|
|
|
:placeholder="f.placeholder || 'مورد جدید را بنویسید و Enter بزنید'"
|
|
|
|
|
|
@keydown.enter.prevent="addChecklistItem(f.key)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<button type="button" class="add-btn" @click="addChecklistItem(f.key)">
|
|
|
|
|
|
<UiIcon name="plus" :size="14" />
|
|
|
|
|
|
افزودن
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else-if="f.type === 'participants'" class="participants">
|
|
|
|
|
|
<div class="p-block">
|
|
|
|
|
|
<small>همکاران (فعالیت در لیست آنها هم نمایش داده میشود)</small>
|
|
|
|
|
|
<UserMultiSelect
|
|
|
|
|
|
:model-value="participants(f.key).userIds"
|
|
|
|
|
|
:users="users"
|
2026-11-03 13:15:21 +03:30
|
|
|
|
:disabled="!canAssignOthers"
|
2026-10-07 12:29:23 +03:30
|
|
|
|
add-label="افزودن همکار"
|
|
|
|
|
|
empty-label="همکاری انتخاب نشده"
|
|
|
|
|
|
@update:model-value="(ids: string[]) => patchParticipants(f.key, { userIds: ids })"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="p-block">
|
|
|
|
|
|
<small>اشخاص / مشتریان</small>
|
|
|
|
|
|
<div class="tags">
|
|
|
|
|
|
<span v-for="p in participants(f.key).people" :key="p.id" class="tag">
|
|
|
|
|
|
<UiIcon name="user" :size="12" />
|
|
|
|
|
|
{{ p.name }}
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
aria-label="حذف"
|
|
|
|
|
|
@click="patchParticipants(f.key, { people: participants(f.key).people.filter((x) => x.id !== p.id) })"
|
|
|
|
|
|
>
|
|
|
|
|
|
×
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="person-search">
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="personQuery[f.key]"
|
|
|
|
|
|
type="search"
|
|
|
|
|
|
placeholder="جستجوی نام یا موبایل شخص…"
|
|
|
|
|
|
@input="searchPeople(f.key)"
|
|
|
|
|
|
@focus="personOpenKey = f.key"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<ul v-if="personOpenKey === f.key && (personResults[f.key]?.length ?? 0) > 0" class="results">
|
|
|
|
|
|
<li v-for="p in personResults[f.key]" :key="p.id">
|
|
|
|
|
|
<button type="button" @click="addPerson(f.key, p)">
|
|
|
|
|
|
<b>{{ p.fullName }}</b>
|
|
|
|
|
|
<small v-if="p.company?.name">{{ p.company.name }}</small>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="p-block">
|
|
|
|
|
|
<small>مهمانان دیگر (نام آزاد)</small>
|
|
|
|
|
|
<div class="tags">
|
|
|
|
|
|
<span v-for="g in participants(f.key).guests" :key="g" class="tag guest">
|
|
|
|
|
|
{{ g }}
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
aria-label="حذف"
|
|
|
|
|
|
@click="patchParticipants(f.key, { guests: participants(f.key).guests.filter((x) => x !== g) })"
|
|
|
|
|
|
>
|
|
|
|
|
|
×
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="add-row">
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="guestDraft[f.key]"
|
|
|
|
|
|
maxlength="120"
|
|
|
|
|
|
placeholder="نام مهمان و Enter"
|
|
|
|
|
|
@keydown.enter.prevent="addGuest(f.key)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<button type="button" class="add-btn" @click="addGuest(f.key)">
|
|
|
|
|
|
<UiIcon name="plus" :size="14" />
|
|
|
|
|
|
افزودن
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.fields-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 0.85rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cf {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 0.35rem;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cf > label {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
color: var(--color-text-muted, #64748b);
|
|
|
|
|
|
font-size: var(--fs-xs, 0.78rem);
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cf > label i {
|
|
|
|
|
|
color: var(--color-danger, #dc2626);
|
|
|
|
|
|
font-style: normal;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cf input:not([type='checkbox']),
|
|
|
|
|
|
.cf select,
|
|
|
|
|
|
.cf textarea {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
|
border-radius: var(--radius);
|
|
|
|
|
|
min-height: var(--control-h);
|
|
|
|
|
|
padding: 0 0.85rem;
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cf textarea {
|
|
|
|
|
|
min-height: 5rem;
|
|
|
|
|
|
padding: 0.6rem 0.85rem;
|
|
|
|
|
|
resize: vertical;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chips,
|
|
|
|
|
|
.tags {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 0.35rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chip-opt {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.25rem;
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
padding: 0.3rem 0.75rem;
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
font-size: 0.82rem;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chip-opt.on {
|
|
|
|
|
|
border-color: #5eead4;
|
|
|
|
|
|
background: #f0fdfa;
|
|
|
|
|
|
color: #0f766e;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.switch {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.6rem;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.switch input {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.switch i {
|
|
|
|
|
|
width: 44px;
|
|
|
|
|
|
height: 26px;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
background: #e2e8f0;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.switch i::after {
|
|
|
|
|
|
content: '';
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset-inline-start: 3px;
|
|
|
|
|
|
top: 3px;
|
|
|
|
|
|
width: 20px;
|
|
|
|
|
|
height: 20px;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
transition: 0.18s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.switch input:checked + i {
|
|
|
|
|
|
background: #14b8a6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.switch input:checked + i::after {
|
|
|
|
|
|
inset-inline-start: 21px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checklist ul {
|
|
|
|
|
|
list-style: none;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 0.35rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checklist li {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.4rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checklist li input[type='checkbox'] {
|
|
|
|
|
|
width: 18px;
|
|
|
|
|
|
height: 18px;
|
|
|
|
|
|
accent-color: #14b8a6;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checklist .item-text {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-height: 2.2rem !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.checklist li.done .item-text {
|
|
|
|
|
|
text-decoration: line-through;
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.progress {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: 0.78rem;
|
|
|
|
|
|
color: #0f766e;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.mini {
|
|
|
|
|
|
width: 1.9rem;
|
|
|
|
|
|
height: 1.9rem;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
font-size: 0.65rem;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.mini:disabled {
|
|
|
|
|
|
opacity: 0.35;
|
|
|
|
|
|
cursor: default;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.mini.danger {
|
|
|
|
|
|
color: #dc2626;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.add-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 0.4rem;
|
|
|
|
|
|
margin-top: 0.35rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.add-row input {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.add-btn {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.25rem;
|
|
|
|
|
|
border: 1px dashed #5eead4;
|
|
|
|
|
|
background: #f0fdfa;
|
|
|
|
|
|
color: #0f766e;
|
|
|
|
|
|
border-radius: var(--radius);
|
|
|
|
|
|
padding: 0 0.8rem;
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
font-size: 0.82rem;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.participants {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 0.75rem;
|
|
|
|
|
|
padding: 0.75rem;
|
|
|
|
|
|
border: 1px dashed #e2e8f0;
|
|
|
|
|
|
border-radius: var(--radius);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.p-block {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 0.35rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.p-block > small {
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
font-size: 0.74rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tag {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 0.25rem;
|
|
|
|
|
|
background: #f0fdfa;
|
|
|
|
|
|
color: #0f766e;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
padding: 0.2rem 0.6rem;
|
|
|
|
|
|
font-size: 0.78rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tag.guest {
|
|
|
|
|
|
background: #f1f5f9;
|
|
|
|
|
|
color: #334155;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tag button {
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
padding: 0 0.1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.person-search {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.results {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset-inline: 0;
|
|
|
|
|
|
top: calc(100% + 4px);
|
|
|
|
|
|
z-index: 20;
|
|
|
|
|
|
list-style: none;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 0.3rem;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
|
border-radius: var(--radius);
|
|
|
|
|
|
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.14);
|
|
|
|
|
|
max-height: 14rem;
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.results button {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 0.1rem;
|
|
|
|
|
|
text-align: start;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
padding: 0.45rem 0.55rem;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.results button:hover {
|
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.results b {
|
|
|
|
|
|
font-size: 0.86rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.results small {
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
font-size: 0.74rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (min-width: 960px) {
|
|
|
|
|
|
.fields-grid {
|
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cf.wide {
|
|
|
|
|
|
grid-column: 1 / -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|