projects: add tracker_url and project create modal
This commit is contained in:
@@ -1130,6 +1130,7 @@ func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.Connect
|
|||||||
"uuid": p.UUID,
|
"uuid": p.UUID,
|
||||||
"owner_username": p.OwnerUsername,
|
"owner_username": p.OwnerUsername,
|
||||||
"name": p.Name,
|
"name": p.Name,
|
||||||
|
"tracker_url": p.TrackerURL,
|
||||||
"is_active": p.IsActive,
|
"is_active": p.IsActive,
|
||||||
"is_system": p.IsSystem,
|
"is_system": p.IsSystem,
|
||||||
"created_at": p.CreatedAt,
|
"created_at": p.CreatedAt,
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ func ProjectToLocal(project *models.Project) *LocalProject {
|
|||||||
UUID: project.UUID,
|
UUID: project.UUID,
|
||||||
OwnerUsername: project.OwnerUsername,
|
OwnerUsername: project.OwnerUsername,
|
||||||
Name: project.Name,
|
Name: project.Name,
|
||||||
|
TrackerURL: project.TrackerURL,
|
||||||
IsActive: project.IsActive,
|
IsActive: project.IsActive,
|
||||||
IsSystem: project.IsSystem,
|
IsSystem: project.IsSystem,
|
||||||
CreatedAt: project.CreatedAt,
|
CreatedAt: project.CreatedAt,
|
||||||
@@ -117,6 +118,7 @@ func LocalToProject(local *LocalProject) *models.Project {
|
|||||||
UUID: local.UUID,
|
UUID: local.UUID,
|
||||||
OwnerUsername: local.OwnerUsername,
|
OwnerUsername: local.OwnerUsername,
|
||||||
Name: local.Name,
|
Name: local.Name,
|
||||||
|
TrackerURL: local.TrackerURL,
|
||||||
IsActive: local.IsActive,
|
IsActive: local.IsActive,
|
||||||
IsSystem: local.IsSystem,
|
IsSystem: local.IsSystem,
|
||||||
CreatedAt: local.CreatedAt,
|
CreatedAt: local.CreatedAt,
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ type LocalProject struct {
|
|||||||
ServerID *uint `json:"server_id,omitempty"`
|
ServerID *uint `json:"server_id,omitempty"`
|
||||||
OwnerUsername string `gorm:"not null;index" json:"owner_username"`
|
OwnerUsername string `gorm:"not null;index" json:"owner_username"`
|
||||||
Name string `gorm:"not null" json:"name"`
|
Name string `gorm:"not null" json:"name"`
|
||||||
|
TrackerURL string `json:"tracker_url"`
|
||||||
IsActive bool `gorm:"default:true;index" json:"is_active"`
|
IsActive bool `gorm:"default:true;index" json:"is_active"`
|
||||||
IsSystem bool `gorm:"default:false;index" json:"is_system"`
|
IsSystem bool `gorm:"default:false;index" json:"is_system"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ type Project struct {
|
|||||||
UUID string `gorm:"size:36;uniqueIndex;not null" json:"uuid"`
|
UUID string `gorm:"size:36;uniqueIndex;not null" json:"uuid"`
|
||||||
OwnerUsername string `gorm:"size:100;not null;index" json:"owner_username"`
|
OwnerUsername string `gorm:"size:100;not null;index" json:"owner_username"`
|
||||||
Name string `gorm:"size:200;not null" json:"name"`
|
Name string `gorm:"size:200;not null" json:"name"`
|
||||||
|
TrackerURL string `gorm:"size:500" json:"tracker_url"`
|
||||||
IsActive bool `gorm:"default:true;index" json:"is_active"`
|
IsActive bool `gorm:"default:true;index" json:"is_active"`
|
||||||
IsSystem bool `gorm:"default:false;index" json:"is_system"`
|
IsSystem bool `gorm:"default:false;index" json:"is_system"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package repository
|
|||||||
import (
|
import (
|
||||||
"git.mchus.pro/mchus/quoteforge/internal/models"
|
"git.mchus.pro/mchus/quoteforge/internal/models"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProjectRepository struct {
|
type ProjectRepository struct {
|
||||||
@@ -21,6 +22,30 @@ func (r *ProjectRepository) Update(project *models.Project) error {
|
|||||||
return r.db.Save(project).Error
|
return r.db.Save(project).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ProjectRepository) UpsertByUUID(project *models.Project) error {
|
||||||
|
if err := r.db.Clauses(clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "uuid"}},
|
||||||
|
DoUpdates: clause.AssignmentColumns([]string{
|
||||||
|
"owner_username",
|
||||||
|
"name",
|
||||||
|
"tracker_url",
|
||||||
|
"is_active",
|
||||||
|
"is_system",
|
||||||
|
"updated_at",
|
||||||
|
}),
|
||||||
|
}).Create(project).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure caller always gets canonical server ID.
|
||||||
|
var persisted models.Project
|
||||||
|
if err := r.db.Where("uuid = ?", project.UUID).First(&persisted).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
project.ID = persisted.ID
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *ProjectRepository) GetByUUID(uuid string) (*models.Project, error) {
|
func (r *ProjectRepository) GetByUUID(uuid string) (*models.Project, error) {
|
||||||
var project models.Project
|
var project models.Project
|
||||||
if err := r.db.Where("uuid = ?", uuid).First(&project).Error; err != nil {
|
if err := r.db.Where("uuid = ?", uuid).First(&project).Error; err != nil {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -28,11 +29,13 @@ func NewProjectService(localDB *localdb.LocalDB) *ProjectService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateProjectRequest struct {
|
type CreateProjectRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
TrackerURL string `json:"tracker_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateProjectRequest struct {
|
type UpdateProjectRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
TrackerURL *string `json:"tracker_url,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectConfigurationsResult struct {
|
type ProjectConfigurationsResult struct {
|
||||||
@@ -52,6 +55,7 @@ func (s *ProjectService) Create(ownerUsername string, req *CreateProjectRequest)
|
|||||||
UUID: uuid.NewString(),
|
UUID: uuid.NewString(),
|
||||||
OwnerUsername: ownerUsername,
|
OwnerUsername: ownerUsername,
|
||||||
Name: name,
|
Name: name,
|
||||||
|
TrackerURL: normalizeProjectTrackerURL(name, req.TrackerURL),
|
||||||
IsActive: true,
|
IsActive: true,
|
||||||
IsSystem: false,
|
IsSystem: false,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
@@ -82,6 +86,11 @@ func (s *ProjectService) Update(projectUUID, ownerUsername string, req *UpdatePr
|
|||||||
}
|
}
|
||||||
|
|
||||||
localProject.Name = name
|
localProject.Name = name
|
||||||
|
if req.TrackerURL != nil {
|
||||||
|
localProject.TrackerURL = normalizeProjectTrackerURL(name, *req.TrackerURL)
|
||||||
|
} else if strings.TrimSpace(localProject.TrackerURL) == "" {
|
||||||
|
localProject.TrackerURL = normalizeProjectTrackerURL(name, "")
|
||||||
|
}
|
||||||
localProject.UpdatedAt = time.Now()
|
localProject.UpdatedAt = time.Now()
|
||||||
localProject.SyncStatus = "pending"
|
localProject.SyncStatus = "pending"
|
||||||
if err := s.localDB.SaveProject(localProject); err != nil {
|
if err := s.localDB.SaveProject(localProject); err != nil {
|
||||||
@@ -260,6 +269,20 @@ func (s *ProjectService) ResolveProjectUUID(ownerUsername string, projectUUID *s
|
|||||||
return &resolved, nil
|
return &resolved, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeProjectTrackerURL(projectCode, trackerURL string) string {
|
||||||
|
trimmedURL := strings.TrimSpace(trackerURL)
|
||||||
|
if trimmedURL != "" {
|
||||||
|
return trimmedURL
|
||||||
|
}
|
||||||
|
|
||||||
|
trimmedCode := strings.TrimSpace(projectCode)
|
||||||
|
if trimmedCode == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return "https://tracker.yandex.ru/" + url.PathEscape(trimmedCode)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ProjectService) enqueueProjectPendingChange(project *localdb.LocalProject, operation string) error {
|
func (s *ProjectService) enqueueProjectPendingChange(project *localdb.LocalProject, operation string) error {
|
||||||
return s.enqueueProjectPendingChangeTx(s.localDB.DB(), project, operation)
|
return s.enqueueProjectPendingChangeTx(s.localDB.DB(), project, operation)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ func (s *Service) ImportProjectsToLocal() (*ProjectImportResult, error) {
|
|||||||
|
|
||||||
existing.OwnerUsername = project.OwnerUsername
|
existing.OwnerUsername = project.OwnerUsername
|
||||||
existing.Name = project.Name
|
existing.Name = project.Name
|
||||||
|
existing.TrackerURL = project.TrackerURL
|
||||||
existing.IsActive = project.IsActive
|
existing.IsActive = project.IsActive
|
||||||
existing.IsSystem = project.IsSystem
|
existing.IsSystem = project.IsSystem
|
||||||
existing.CreatedAt = project.CreatedAt
|
existing.CreatedAt = project.CreatedAt
|
||||||
@@ -757,20 +758,8 @@ func (s *Service) pushProjectChange(change *localdb.PendingChange) error {
|
|||||||
project := payload.Snapshot
|
project := payload.Snapshot
|
||||||
project.UUID = payload.ProjectUUID
|
project.UUID = payload.ProjectUUID
|
||||||
|
|
||||||
serverProject, err := projectRepo.GetByUUID(project.UUID)
|
if err := projectRepo.UpsertByUUID(&project); err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("upsert project on server: %w", err)
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
if createErr := projectRepo.Create(&project); createErr != nil {
|
|
||||||
return fmt.Errorf("create project on server: %w", createErr)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("get project on server: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
project.ID = serverProject.ID
|
|
||||||
if updateErr := projectRepo.Update(&project); updateErr != nil {
|
|
||||||
return fmt.Errorf("update project on server: %w", updateErr)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
localProject, localErr := s.localDB.GetProjectByUUID(project.UUID)
|
localProject, localErr := s.localDB.GetProjectByUUID(project.UUID)
|
||||||
@@ -1032,7 +1021,7 @@ func (s *Service) ensureConfigurationProject(mariaDB *gorm.DB, cfg *models.Confi
|
|||||||
if modelProject.OwnerUsername == "" {
|
if modelProject.OwnerUsername == "" {
|
||||||
modelProject.OwnerUsername = cfg.OwnerUsername
|
modelProject.OwnerUsername = cfg.OwnerUsername
|
||||||
}
|
}
|
||||||
if createErr := projectRepo.Create(modelProject); createErr != nil {
|
if createErr := projectRepo.UpsertByUUID(modelProject); createErr != nil {
|
||||||
return createErr
|
return createErr
|
||||||
}
|
}
|
||||||
if modelProject.ID > 0 {
|
if modelProject.ID > 0 {
|
||||||
|
|||||||
@@ -65,6 +65,54 @@ func TestPushPendingChangesProjectsBeforeConfigurations(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPushPendingChangesProjectCreateThenUpdateBeforeFirstPush(t *testing.T) {
|
||||||
|
local := newLocalDBForSyncTest(t)
|
||||||
|
serverDB := newServerDBForSyncTest(t)
|
||||||
|
|
||||||
|
localSync := syncsvc.NewService(nil, local)
|
||||||
|
projectService := services.NewProjectService(local)
|
||||||
|
configService := services.NewLocalConfigurationService(local, localSync, &services.QuoteService{}, func() bool { return false })
|
||||||
|
pushService := syncsvc.NewServiceWithDB(serverDB, local)
|
||||||
|
|
||||||
|
project, err := projectService.Create("tester", &services.CreateProjectRequest{Name: "Project v1"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create project: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := projectService.Update(project.UUID, "tester", &services.UpdateProjectRequest{Name: "Project v2"}); err != nil {
|
||||||
|
t.Fatalf("update project: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := configService.Create("tester", &services.CreateConfigRequest{
|
||||||
|
Name: "Cfg linked",
|
||||||
|
Items: models.ConfigItems{{LotName: "CPU_A", Quantity: 1, UnitPrice: 1000}},
|
||||||
|
ServerCount: 1,
|
||||||
|
ProjectUUID: &project.UUID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := pushService.PushPendingChanges(); err != nil {
|
||||||
|
t.Fatalf("push pending changes: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverProject models.Project
|
||||||
|
if err := serverDB.Where("uuid = ?", project.UUID).First(&serverProject).Error; err != nil {
|
||||||
|
t.Fatalf("project not pushed to server: %v", err)
|
||||||
|
}
|
||||||
|
if serverProject.Name != "Project v2" {
|
||||||
|
t.Fatalf("expected latest project name, got %q", serverProject.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverCfg models.Configuration
|
||||||
|
if err := serverDB.Where("uuid = ?", cfg.UUID).First(&serverCfg).Error; err != nil {
|
||||||
|
t.Fatalf("configuration not pushed to server: %v", err)
|
||||||
|
}
|
||||||
|
if serverCfg.ProjectUUID == nil || *serverCfg.ProjectUUID != project.UUID {
|
||||||
|
t.Fatalf("expected project_uuid=%s on pushed config, got %v", project.UUID, serverCfg.ProjectUUID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPushPendingChangesSkipsStaleUpdateAndAppliesLatest(t *testing.T) {
|
func TestPushPendingChangesSkipsStaleUpdateAndAppliesLatest(t *testing.T) {
|
||||||
local := newLocalDBForSyncTest(t)
|
local := newLocalDBForSyncTest(t)
|
||||||
serverDB := newServerDBForSyncTest(t)
|
serverDB := newServerDBForSyncTest(t)
|
||||||
@@ -277,6 +325,7 @@ CREATE TABLE qt_projects (
|
|||||||
uuid TEXT NOT NULL UNIQUE,
|
uuid TEXT NOT NULL UNIQUE,
|
||||||
owner_username TEXT NOT NULL,
|
owner_username TEXT NOT NULL,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
|
tracker_url TEXT NULL,
|
||||||
is_active INTEGER NOT NULL DEFAULT 1,
|
is_active INTEGER NOT NULL DEFAULT 1,
|
||||||
is_system INTEGER NOT NULL DEFAULT 0,
|
is_system INTEGER NOT NULL DEFAULT 0,
|
||||||
created_at DATETIME,
|
created_at DATETIME,
|
||||||
|
|||||||
7
migrations/012_add_project_tracker_url.sql
Normal file
7
migrations/012_add_project_tracker_url.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
ALTER TABLE qt_projects
|
||||||
|
ADD COLUMN tracker_url VARCHAR(500) NULL AFTER name;
|
||||||
|
|
||||||
|
UPDATE qt_projects
|
||||||
|
SET tracker_url = CONCAT('https://tracker.yandex.ru/', TRIM(name))
|
||||||
|
WHERE (tracker_url IS NULL OR tracker_url = '')
|
||||||
|
AND TRIM(COALESCE(name, '')) <> '';
|
||||||
@@ -125,6 +125,12 @@ function escapeHtml(text) {
|
|||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveProjectTrackerURL(projectData) {
|
||||||
|
if (!projectData) return '';
|
||||||
|
const explicitURL = (projectData.tracker_url || '').trim();
|
||||||
|
return explicitURL;
|
||||||
|
}
|
||||||
|
|
||||||
function setConfigStatusMode(mode) {
|
function setConfigStatusMode(mode) {
|
||||||
if (mode !== 'active' && mode !== 'archived') return;
|
if (mode !== 'active' && mode !== 'archived') return;
|
||||||
configStatusMode = mode;
|
configStatusMode = mode;
|
||||||
@@ -224,8 +230,18 @@ async function loadProject() {
|
|||||||
project = await resp.json();
|
project = await resp.json();
|
||||||
document.getElementById('project-title').textContent = project.name;
|
document.getElementById('project-title').textContent = project.name;
|
||||||
const trackerLink = document.getElementById('tracker-link');
|
const trackerLink = document.getElementById('tracker-link');
|
||||||
if (trackerLink && project && project.name) {
|
if (trackerLink) {
|
||||||
trackerLink.href = 'https://tracker.yandex.ru/' + encodeURIComponent(project.name);
|
if (project && project.is_system) {
|
||||||
|
trackerLink.classList.add('hidden');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const trackerURL = resolveProjectTrackerURL(project);
|
||||||
|
if (trackerURL) {
|
||||||
|
trackerLink.href = trackerURL;
|
||||||
|
trackerLink.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
trackerLink.classList.add('hidden');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<a href="/configs" class="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300">
|
<a href="/configs" class="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300">
|
||||||
Все конфигурации
|
Все конфигурации
|
||||||
</a>
|
</a>
|
||||||
<button onclick="createProject()" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
|
<button onclick="openCreateProjectModal()" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
|
||||||
+ Новый проект
|
+ Новый проект
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -27,6 +27,28 @@
|
|||||||
<div id="projects-table" class="bg-white rounded-lg shadow p-4 text-gray-500">Загрузка...</div>
|
<div id="projects-table" class="bg-white rounded-lg shadow p-4 text-gray-500">Загрузка...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="create-project-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||||||
|
<div class="bg-white rounded-lg shadow-xl w-full max-w-md mx-4 p-6">
|
||||||
|
<h2 class="text-xl font-semibold mb-4">Новый проект</h2>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="create-project-code" class="block text-sm font-medium text-gray-700 mb-1">Код проекта</label>
|
||||||
|
<input id="create-project-code" type="text" placeholder="Например: OPS-123"
|
||||||
|
class="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="create-project-tracker-url" class="block text-sm font-medium text-gray-700 mb-1">Ссылка на трекер</label>
|
||||||
|
<input id="create-project-tracker-url" type="url" placeholder="https://tracker.yandex.ru/OPS-123"
|
||||||
|
class="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-2 mt-6">
|
||||||
|
<button type="button" onclick="closeCreateProjectModal()" class="px-4 py-2 text-gray-600 hover:text-gray-800">Отмена</button>
|
||||||
|
<button type="button" onclick="createProject()" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Создать</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let status = 'active';
|
let status = 'active';
|
||||||
let projectsSearch = '';
|
let projectsSearch = '';
|
||||||
@@ -35,6 +57,10 @@ let currentPage = 1;
|
|||||||
let perPage = 10;
|
let perPage = 10;
|
||||||
let sortField = 'created_at';
|
let sortField = 'created_at';
|
||||||
let sortDir = 'desc';
|
let sortDir = 'desc';
|
||||||
|
let createProjectTrackerManuallyEdited = false;
|
||||||
|
let createProjectLastAutoTrackerURL = '';
|
||||||
|
|
||||||
|
const trackerBaseURL = 'https://tracker.yandex.ru/';
|
||||||
|
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
@@ -218,18 +244,60 @@ function goToPage(page) {
|
|||||||
loadProjects();
|
loadProjects();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildTrackerURLFromProjectCode(projectCode) {
|
||||||
|
const code = (projectCode || '').trim();
|
||||||
|
if (!code) return '';
|
||||||
|
return trackerBaseURL + encodeURIComponent(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateProjectModal() {
|
||||||
|
const codeInput = document.getElementById('create-project-code');
|
||||||
|
const trackerInput = document.getElementById('create-project-tracker-url');
|
||||||
|
codeInput.value = '';
|
||||||
|
trackerInput.value = '';
|
||||||
|
createProjectTrackerManuallyEdited = false;
|
||||||
|
createProjectLastAutoTrackerURL = '';
|
||||||
|
document.getElementById('create-project-modal').classList.remove('hidden');
|
||||||
|
document.getElementById('create-project-modal').classList.add('flex');
|
||||||
|
codeInput.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCreateProjectModal() {
|
||||||
|
document.getElementById('create-project-modal').classList.add('hidden');
|
||||||
|
document.getElementById('create-project-modal').classList.remove('flex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCreateProjectTrackerURL() {
|
||||||
|
const codeInput = document.getElementById('create-project-code');
|
||||||
|
const trackerInput = document.getElementById('create-project-tracker-url');
|
||||||
|
const generatedURL = buildTrackerURLFromProjectCode(codeInput.value);
|
||||||
|
if (!createProjectTrackerManuallyEdited || trackerInput.value.trim() === '' || trackerInput.value === createProjectLastAutoTrackerURL) {
|
||||||
|
trackerInput.value = generatedURL;
|
||||||
|
createProjectLastAutoTrackerURL = generatedURL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function createProject() {
|
async function createProject() {
|
||||||
const name = prompt('Название проекта');
|
const codeInput = document.getElementById('create-project-code');
|
||||||
if (!name || !name.trim()) return;
|
const trackerInput = document.getElementById('create-project-tracker-url');
|
||||||
|
const name = (codeInput.value || '').trim();
|
||||||
|
if (!name) {
|
||||||
|
alert('Введите код проекта');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const resp = await fetch('/api/projects', {
|
const resp = await fetch('/api/projects', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({name: name.trim()})
|
body: JSON.stringify({
|
||||||
|
name: name,
|
||||||
|
tracker_url: (trackerInput.value || '').trim()
|
||||||
|
})
|
||||||
});
|
});
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
alert('Не удалось создать проект');
|
alert('Не удалось создать проект');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
closeCreateProjectModal();
|
||||||
loadProjects();
|
loadProjects();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,6 +392,34 @@ document.getElementById('projects-search').addEventListener('input', function(e)
|
|||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
loadProjects();
|
loadProjects();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('create-project-code').addEventListener('input', function() {
|
||||||
|
updateCreateProjectTrackerURL();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('create-project-tracker-url').addEventListener('input', function(e) {
|
||||||
|
createProjectTrackerManuallyEdited = (e.target.value || '').trim() !== createProjectLastAutoTrackerURL;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('create-project-code').addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
createProject();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('create-project-tracker-url').addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
createProject();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('create-project-modal').addEventListener('click', function(e) {
|
||||||
|
if (e.target === this) {
|
||||||
|
closeCreateProjectModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user