3e969e29dc
Модель и сервис событий календаря, вью и страница календаря. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
205 lines
17 KiB
HTML
205 lines
17 KiB
HTML
{% load i18n %}
|
||
<div class="page-content" data-cal-scope="{{ cal_scope }}">
|
||
|
||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:1.5rem; flex-wrap:wrap; gap:0.75rem;">
|
||
<div>
|
||
<h1 style="font-size:1.25rem; font-weight:500; color:var(--color-foreground);">{% trans "Календарь" %}</h1>
|
||
<p style="font-size:0.875rem; color:var(--color-muted-foreground); margin-top:0.25rem;">{% trans "Праздники, отпуска, смены, дни рождения и события" %}</p>
|
||
</div>
|
||
<button type="button" class="btn btn-primary" onclick="calEventFormToggle(true)">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<line x1="12" x2="12" y1="5" y2="19"/><line x1="5" x2="19" y1="12" y2="12"/>
|
||
</svg>
|
||
{% trans "Новое событие" %}
|
||
</button>
|
||
</div>
|
||
|
||
{% if cal_form_message %}
|
||
<div style="margin-bottom:1rem; padding:0.75rem 1rem; border-radius:0.5rem; border:1px solid hsl(152 40% 70%); background:color-mix(in srgb, var(--color-success) 12%, var(--color-card)); color:var(--color-foreground); font-size:0.875rem;">
|
||
{{ cal_form_message }}
|
||
</div>
|
||
{% endif %}
|
||
{% if cal_form_errors %}
|
||
<div style="margin-bottom:1rem; padding:0.75rem 1rem; border-radius:0.5rem; background:hsl(0 70% 96%); border:1px solid hsl(0 70% 85%); color:hsl(0 55% 35%); font-size:0.8125rem;">
|
||
{% for err in cal_form_errors %}<div>{{ err }}</div>{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div id="cal-event-form-wrap" style="{% if not cal_form_open %}display:none;{% endif %} margin-bottom:1.25rem;">
|
||
<form method="post" action="/calendar/events/"
|
||
hx-post="/calendar/events/" hx-target="#main-content" hx-indicator="#loader, find .btn-label-loading" hx-disabled-elt="find button[type=submit]"
|
||
style="background:var(--color-card); border:1px solid var(--color-border); border-radius:0.75rem; padding:1.25rem; display:flex; flex-direction:column; gap:0.75rem;">
|
||
{% csrf_token %}
|
||
<input type="hidden" name="month" value="{{ cal_month_str }}">
|
||
<input type="hidden" name="scope" value="{{ cal_scope }}">
|
||
<div style="display:flex; align-items:center; justify-content:space-between;">
|
||
<span style="font-size:0.9375rem; font-weight:600; color:var(--color-foreground);">{% trans "Новое событие" %}</span>
|
||
<button type="button" class="btn btn-ghost btn-icon" onclick="calEventFormToggle(false)" aria-label="{% trans 'Закрыть' %}">×</button>
|
||
</div>
|
||
<div style="display:grid; grid-template-columns:2fr 1fr 1fr; gap:0.75rem;">
|
||
<div>
|
||
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Название" %} *</label>
|
||
<input type="text" name="title" required class="input" style="width:100%;" value="{{ cal_form_data.title|default:'' }}">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Тип" %}</label>
|
||
<select name="event_type" class="input" style="width:100%;">
|
||
{% for value, label in cal_event_types %}
|
||
<option value="{{ value }}" {% if cal_form_data.event_type == value %}selected{% endif %}>{{ label }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Видимость" %}</label>
|
||
<select name="visibility" class="input" style="width:100%;">
|
||
<option value="company" {% if cal_form_data.visibility != 'personal' %}selected{% endif %}>{% trans "Компания" %}</option>
|
||
<option value="personal" {% if cal_form_data.visibility == 'personal' %}selected{% endif %}>{% trans "Личное" %}</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div style="display:grid; grid-template-columns:repeat(4,1fr); gap:0.75rem;">
|
||
<div>
|
||
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Дата с" %} *</label>
|
||
<input type="date" name="date_from" required class="input" style="width:100%;" value="{{ cal_form_data.date_from|default:cal_default_date }}">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Дата по" %}</label>
|
||
<input type="date" name="date_to" class="input" style="width:100%;" value="{{ cal_form_data.date_to|default:'' }}">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Время с" %}</label>
|
||
<input type="time" name="time_from" class="input" style="width:100%;" value="{{ cal_form_data.time_from|default:'' }}">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Время по" %}</label>
|
||
<input type="time" name="time_to" class="input" style="width:100%;" value="{{ cal_form_data.time_to|default:'' }}">
|
||
</div>
|
||
</div>
|
||
<div style="display:grid; grid-template-columns:1fr 2fr; gap:0.75rem;">
|
||
<div>
|
||
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Место" %}</label>
|
||
<input type="text" name="location" class="input" style="width:100%;" value="{{ cal_form_data.location|default:'' }}">
|
||
</div>
|
||
<div>
|
||
<label style="display:block; font-size:0.75rem; color:var(--color-muted-foreground); margin-bottom:0.25rem;">{% trans "Примечания" %}</label>
|
||
<input type="text" name="notes" class="input" style="width:100%;" value="{{ cal_form_data.notes|default:'' }}">
|
||
</div>
|
||
</div>
|
||
<div style="display:flex; justify-content:flex-end; gap:0.5rem;">
|
||
<button type="button" class="btn btn-secondary" onclick="calEventFormToggle(false)">{% trans "Отмена" %}</button>
|
||
<button type="submit" class="btn btn-primary">
|
||
{% trans "Сохранить" %}
|
||
<span class="btn-label-loading htmx-indicator">…</span>
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<div style="background:var(--color-card); border-radius:0.75rem; border:1px solid var(--color-border); box-shadow:0 1px 2px rgb(0 0 0/0.05); padding:1.5rem;">
|
||
|
||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:1.25rem; flex-wrap:wrap; gap:0.75rem;">
|
||
<div style="display:flex; align-items:center; gap:0.5rem;">
|
||
<a href="/calendar/?month={{ cal_prev_month }}&scope={{ cal_scope }}"
|
||
hx-get="/calendar/?month={{ cal_prev_month }}&scope={{ cal_scope }}"
|
||
hx-target="#main-content" hx-push-url="true" hx-indicator="#loader"
|
||
class="btn btn-ghost btn-icon">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
|
||
</a>
|
||
<span style="font-size:1rem; font-weight:600; color:var(--color-foreground); min-width:10rem; text-align:center;">{{ cal_month_label }}</span>
|
||
<a href="/calendar/?month={{ cal_next_month }}&scope={{ cal_scope }}"
|
||
hx-get="/calendar/?month={{ cal_next_month }}&scope={{ cal_scope }}"
|
||
hx-target="#main-content" hx-push-url="true" hx-indicator="#loader"
|
||
class="btn btn-ghost btn-icon">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
|
||
</a>
|
||
</div>
|
||
<div style="display:flex; gap:0.25rem; background:var(--color-muted); border-radius:var(--radius); padding:0.25rem;">
|
||
<a href="/calendar/?month={{ cal_month_str }}&scope=mine"
|
||
hx-get="/calendar/?month={{ cal_month_str }}&scope=mine"
|
||
hx-target="#main-content" hx-push-url="true" hx-indicator="#loader"
|
||
class="btn" style="font-size:0.8125rem; padding:0.3rem 0.875rem; border:none; text-decoration:none;{% if cal_scope == 'mine' %} background:var(--color-card); color:var(--color-card-foreground); box-shadow:0 1px 2px rgb(0 0 0/0.08);{% else %} background:transparent; color:var(--color-muted-foreground);{% endif %}">{% trans "Мои" %}</a>
|
||
<a href="/calendar/?month={{ cal_month_str }}&scope=team"
|
||
hx-get="/calendar/?month={{ cal_month_str }}&scope=team"
|
||
hx-target="#main-content" hx-push-url="true" hx-indicator="#loader"
|
||
class="btn" style="font-size:0.8125rem; padding:0.3rem 0.875rem; border:none; text-decoration:none;{% if cal_scope == 'team' %} background:var(--color-card); color:var(--color-card-foreground); box-shadow:0 1px 2px rgb(0 0 0/0.08);{% else %} background:transparent; color:var(--color-muted-foreground);{% endif %}">{% trans "Команда" %}</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:grid; grid-template-columns:repeat(7,1fr); gap:1px; margin-bottom:0.5rem;">
|
||
<div style="text-align:center; font-size:0.75rem; font-weight:600; color:var(--color-muted-foreground); padding:0.5rem;">{% trans "Пн" %}</div>
|
||
<div style="text-align:center; font-size:0.75rem; font-weight:600; color:var(--color-muted-foreground); padding:0.5rem;">{% trans "Вт" %}</div>
|
||
<div style="text-align:center; font-size:0.75rem; font-weight:600; color:var(--color-muted-foreground); padding:0.5rem;">{% trans "Ср" %}</div>
|
||
<div style="text-align:center; font-size:0.75rem; font-weight:600; color:var(--color-muted-foreground); padding:0.5rem;">{% trans "Чт" %}</div>
|
||
<div style="text-align:center; font-size:0.75rem; font-weight:600; color:var(--color-muted-foreground); padding:0.5rem;">{% trans "Пт" %}</div>
|
||
<div style="text-align:center; font-size:0.75rem; font-weight:600; color:var(--color-destructive); padding:0.5rem;">{% trans "Сб" %}</div>
|
||
<div style="text-align:center; font-size:0.75rem; font-weight:600; color:var(--color-destructive); padding:0.5rem;">{% trans "Нд" %}</div>
|
||
</div>
|
||
|
||
<div style="display:grid; grid-template-columns:repeat(7,1fr); gap:4px;" id="cal-grid">
|
||
{% for week in cal_weeks %}
|
||
{% for cell in week %}
|
||
{% if cell %}
|
||
<div style="min-height:6.5rem; padding:0.375rem; border:1px solid {% if cell.is_today %}var(--color-primary){% else %}var(--color-border){% endif %}; border-radius:0.375rem; background:{% if cell.is_today %}color-mix(in srgb, var(--color-primary) 6%, var(--color-background)){% elif cell.is_holiday or cell.is_weekend %}var(--color-muted){% else %}var(--color-background){% endif %};{% if cell.is_today %} border-width:2px;{% endif %}">
|
||
<span style="font-size:0.8125rem; font-weight:{% if cell.is_today %}600{% else %}500{% endif %}; color:{% if cell.is_today %}var(--color-primary){% elif cell.is_weekend or cell.is_holiday %}var(--color-muted-foreground){% else %}var(--color-foreground){% endif %};">{{ cell.day }}</span>
|
||
<div style="margin-top:0.25rem; display:flex; flex-direction:column; gap:2px;">
|
||
{% for ev in cell.events %}
|
||
<div class="{{ ev.css }}" title="{{ ev.label }}"
|
||
style="font-size:0.6875rem; padding:2px 5px; border-radius:3px; line-height:1.25; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; display:flex; align-items:center; gap:0.25rem;">
|
||
<span style="min-width:0; overflow:hidden; text-overflow:ellipsis;">{{ ev.label }}</span>
|
||
{% if ev.kind == 'event' and ev.meta.can_delete %}
|
||
<form method="post" action="/calendar/events/{{ ev.meta.id }}/delete/"
|
||
hx-post="/calendar/events/{{ ev.meta.id }}/delete/"
|
||
hx-target="#main-content" hx-indicator="#loader"
|
||
style="margin:0; flex:none;"
|
||
onsubmit="return confirm('{% trans "Удалить событие?" %}');">
|
||
{% csrf_token %}
|
||
<input type="hidden" name="month" value="{{ cal_month_str }}">
|
||
<input type="hidden" name="scope" value="{{ cal_scope }}">
|
||
<button type="submit" style="border:none; background:transparent; cursor:pointer; padding:0; font-size:0.75rem; line-height:1; color:inherit; opacity:0.7;" title="{% trans 'Удалить' %}">×</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
{% else %}
|
||
<div style="min-height:6.5rem;"></div>
|
||
{% endif %}
|
||
{% endfor %}
|
||
{% endfor %}
|
||
</div>
|
||
|
||
<div style="display:flex; align-items:center; gap:1.25rem; margin-top:1.25rem; padding-top:1rem; border-top:1px solid var(--color-border); flex-wrap:wrap;">
|
||
<div style="display:flex; align-items:center; gap:0.375rem; font-size:0.75rem; color:var(--color-muted-foreground);">
|
||
<span class="cal-chip-holiday" style="display:inline-block; width:10px; height:10px; border-radius:2px;"></span>{% trans "Праздник" %}
|
||
</div>
|
||
<div style="display:flex; align-items:center; gap:0.375rem; font-size:0.75rem; color:var(--color-muted-foreground);">
|
||
<span class="cal-chip-leave" style="display:inline-block; width:10px; height:10px; border-radius:2px;"></span>{% trans "Отпуск" %}
|
||
</div>
|
||
<div style="display:flex; align-items:center; gap:0.375rem; font-size:0.75rem; color:var(--color-muted-foreground);">
|
||
<span class="cal-chip-birthday" style="display:inline-block; width:10px; height:10px; border-radius:2px;"></span>{% trans "День рождения" %}
|
||
</div>
|
||
<div style="display:flex; align-items:center; gap:0.375rem; font-size:0.75rem; color:var(--color-muted-foreground);">
|
||
<span class="cal-chip-shift" style="display:inline-block; width:10px; height:10px; border-radius:2px;"></span>{% trans "Смена" %}
|
||
</div>
|
||
<div style="display:flex; align-items:center; gap:0.375rem; font-size:0.75rem; color:var(--color-muted-foreground);">
|
||
<span class="cal-chip-interview" style="display:inline-block; width:10px; height:10px; border-radius:2px;"></span>{% trans "Собеседование" %}
|
||
</div>
|
||
<div style="display:flex; align-items:center; gap:0.375rem; font-size:0.75rem; color:var(--color-muted-foreground);">
|
||
<span class="cal-chip-event" style="display:inline-block; width:10px; height:10px; border-radius:2px;"></span>{% trans "Событие" %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
.cal-chip-holiday { background:#fef2f2; color:#b91c1c; border:1px solid #fecaca; }
|
||
.cal-chip-leave { background:#f0fdf4; color:#15803d; border:1px solid #bbf7d0; }
|
||
.cal-chip-leave-pending { background:#fff7ed; color:#c2410c; border:1px solid #fed7aa; }
|
||
.cal-chip-birthday { background:#fdf4ff; color:#a21caf; border:1px solid #f5d0fe; }
|
||
.cal-chip-shift { background:#f8fafc; color:#475569; border:1px solid #e2e8f0; }
|
||
.cal-chip-interview { background:#eff6ff; color:#1d4ed8; border:1px solid #bfdbfe; }
|
||
.cal-chip-meeting { background:#ecfeff; color:#0e7490; border:1px solid #a5f3fc; }
|
||
.cal-chip-event { background:#fef9c3; color:#a16207; border:1px solid #fde68a; }
|
||
</style>
|