Pretty all the things I missed
My laptop didn't have lefthook running. Oops.
This commit is contained in:
parent
f60bde7fd9
commit
4bbfbdb9e6
30 changed files with 490 additions and 487 deletions
|
|
@ -119,7 +119,7 @@ func handlerJSONPost[ReqType any](f handlerFunctionPost[ReqType]) http.HandlerFu
|
|||
ctx := r.Context()
|
||||
path, e := f(ctx, r, *req)
|
||||
if e != nil {
|
||||
return
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, path, http.StatusFound)
|
||||
}
|
||||
|
|
@ -128,6 +128,7 @@ func handlerJSONPost[ReqType any](f handlerFunctionPost[ReqType]) http.HandlerFu
|
|||
type postMultipartResponse struct {
|
||||
URI string `json:"uri"`
|
||||
}
|
||||
|
||||
func authenticatedHandlerPostMultipart(f handlerFunctionPostAuthenticated[[]file.Upload], collection file.Collection) http.Handler {
|
||||
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) {
|
||||
err := r.ParseMultipartForm(32 << 10) // 32 MB buffer
|
||||
|
|
@ -142,11 +143,11 @@ func authenticatedHandlerPostMultipart(f handlerFunctionPostAuthenticated[[]file
|
|||
}
|
||||
|
||||
/*
|
||||
err = decoder.Decode(&content, r.PostForm)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Failed to decode form: %w", err)
|
||||
return
|
||||
}
|
||||
err = decoder.Decode(&content, r.PostForm)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Failed to decode form: %w", err)
|
||||
return
|
||||
}
|
||||
*/
|
||||
ctx := r.Context()
|
||||
path, e := f(ctx, r, u, uploads)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ type reqSignin struct {
|
|||
Password string `json:"password"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
func postSignin(ctx context.Context, r *http.Request, req reqSignin) (string, *nhttp.ErrorWithStatus) {
|
||||
if req.Password == "" {
|
||||
return "", nhttp.NewErrorStatus(http.StatusBadRequest, "Empty password")
|
||||
|
|
|
|||
|
|
@ -9,12 +9,14 @@ import (
|
|||
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type reqSignup struct {
|
||||
Username string `json:"username"`
|
||||
Name string `json:"name"`
|
||||
Name string `json:"name"`
|
||||
Password string `json:"password"`
|
||||
Terms bool `json:"terms"`
|
||||
Terms bool `json:"terms"`
|
||||
}
|
||||
|
||||
func postSignup(ctx context.Context, r *http.Request, signup reqSignup) (string, *nhttp.ErrorWithStatus) {
|
||||
|
||||
log.Info().Str("username", signup.Username).Str("name", signup.Name).Str("password", strings.Repeat("*", len(signup.Password))).Msg("Signup")
|
||||
|
|
@ -33,4 +35,3 @@ func postSignup(ctx context.Context, r *http.Request, signup reqSignup) (string,
|
|||
|
||||
return "/", nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
|
||||
)
|
||||
|
||||
type ServiceRequestSummary struct {
|
||||
Date time.Time
|
||||
Location string
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ func SignalList(ctx context.Context, user User, limit int) ([]*Signal, error) {
|
|||
row.Pool = p
|
||||
row.Report = nil
|
||||
} else if row.Report.ID != 0 {
|
||||
report, ok := report_map[row.Report.ID]
|
||||
report, ok := report_map[row.Report.ID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to get report %d for %d", row.Report.ID, row.ID)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ func newUser(ctx context.Context, org Organization, user *models.User) User {
|
|||
func CreateUser(ctx context.Context, username string, name string, password_hash string) (*User, error) {
|
||||
o_setter := models.OrganizationSetter{
|
||||
IsCatchall: omit.From(false),
|
||||
Name: omit.From(fmt.Sprintf("%s's organization", username)),
|
||||
Name: omit.From(fmt.Sprintf("%s's organization", username)),
|
||||
}
|
||||
o, err := models.Organizations.Insert(&o_setter).One(ctx, db.PGInstance.BobDB)
|
||||
if err != nil {
|
||||
|
|
|
|||
22
sync/cell.go
22
sync/cell.go
|
|
@ -38,15 +38,15 @@ func getCellDetails(ctx context.Context, r *http.Request, user platform.User) (*
|
|||
return nil, nhttp.NewError("Failed to get inspections by cell: %w", err)
|
||||
}
|
||||
/*
|
||||
center, err := h3.Cell(c).LatLng()
|
||||
if err != nil {
|
||||
return nil, nhttp.NewError("Failed to get center: %w", err)
|
||||
}
|
||||
geojson, err := h3utils.H3ToGeoJSON([]h3.Cell{h3.Cell(c)})
|
||||
if err != nil {
|
||||
return nil, nhttp.NewError("Failed to get boundaries: %w", err)
|
||||
}
|
||||
resolution := h3.Cell(c).Resolution()
|
||||
center, err := h3.Cell(c).LatLng()
|
||||
if err != nil {
|
||||
return nil, nhttp.NewError("Failed to get center: %w", err)
|
||||
}
|
||||
geojson, err := h3utils.H3ToGeoJSON([]h3.Cell{h3.Cell(c)})
|
||||
if err != nil {
|
||||
return nil, nhttp.NewError("Failed to get boundaries: %w", err)
|
||||
}
|
||||
resolution := h3.Cell(c).Resolution()
|
||||
*/
|
||||
sources, err := platform.BreedingSourcesByCell(ctx, user.Organization, h3.Cell(c))
|
||||
if err != nil {
|
||||
|
|
@ -65,7 +65,7 @@ func getCellDetails(ctx context.Context, r *http.Request, user platform.User) (*
|
|||
BreedingSources: sources,
|
||||
CellBoundary: boundary,
|
||||
Inspections: inspections,
|
||||
Traps: traps,
|
||||
Treatments: treatments,
|
||||
Traps: traps,
|
||||
Treatments: treatments,
|
||||
}), nil
|
||||
}
|
||||
|
|
|
|||
14
sync/dash.go
14
sync/dash.go
|
|
@ -21,8 +21,8 @@ type contentSource struct {
|
|||
User platform.User
|
||||
}
|
||||
type contentTrap struct {
|
||||
Trap platform.Trap
|
||||
User platform.User
|
||||
Trap platform.Trap
|
||||
User platform.User
|
||||
}
|
||||
type contentLayoutTest struct {
|
||||
User platform.User
|
||||
|
|
@ -71,7 +71,7 @@ func getSource(ctx context.Context, r *http.Request, user platform.User) (*html.
|
|||
}
|
||||
treatment_models := platform.ModelTreatment(treatments)
|
||||
data := contentSource{
|
||||
Inspections: inspections,
|
||||
Inspections: inspections,
|
||||
Source: s,
|
||||
Traps: traps,
|
||||
Treatments: treatments,
|
||||
|
|
@ -99,10 +99,10 @@ func getTrap(ctx context.Context, r *http.Request, user platform.User) (*html.Re
|
|||
return nil, nhttp.NewError("Failed to get trap: %w", err)
|
||||
}
|
||||
/*
|
||||
latlng, err := t.H3Cell.LatLng()
|
||||
if err != nil {
|
||||
return nil, nhttp.NewError("Failed to get latlng: %w", err)
|
||||
}
|
||||
latlng, err := t.H3Cell.LatLng()
|
||||
if err != nil {
|
||||
return nil, nhttp.NewError("Failed to get latlng: %w", err)
|
||||
}
|
||||
*/
|
||||
data := contentTrap{
|
||||
Trap: *t,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
package sync
|
||||
|
||||
import (
|
||||
)
|
||||
import ()
|
||||
|
|
|
|||
26
ts/client.ts
26
ts/client.ts
|
|
@ -1,24 +1,24 @@
|
|||
// src/api/axios.js or similar
|
||||
import axios from 'axios';
|
||||
import router from '@/router';
|
||||
import axios from "axios";
|
||||
import router from "@/router";
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: '/api',
|
||||
withCredentials: true
|
||||
baseURL: "/api",
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
// Response interceptor to catch auth failures
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response && error.response.status === 401) {
|
||||
// Session expired or not authenticated
|
||||
router.push('/login');
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response && error.response.status === 401) {
|
||||
// Session expired or not authenticated
|
||||
router.push("/login");
|
||||
}
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
apiClient.isAuthenticated = () => {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
export default apiClient;
|
||||
|
|
|
|||
|
|
@ -1,209 +1,220 @@
|
|||
<style scoped>
|
||||
.upload-widget {
|
||||
max-width: 400px;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 400px;
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
position: relative;
|
||||
border: 2px dashed #cbd5e0;
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
background-color: #f7fafc;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
border: 2px dashed #cbd5e0;
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
background-color: #f7fafc;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.upload-area:hover {
|
||||
border-color: #4299e1;
|
||||
background-color: #ebf8ff;
|
||||
border-color: #4299e1;
|
||||
background-color: #ebf8ff;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-input:disabled {
|
||||
cursor: not-allowed;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.upload-info {
|
||||
pointer-events: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #718096;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.placeholder svg {
|
||||
margin: 0 auto 1rem;
|
||||
color: #a0aec0;
|
||||
margin: 0 auto 1rem;
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
.placeholder p {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.file-selected {
|
||||
color: #2d3748;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
margin: 0 0 0.5rem;
|
||||
font-weight: 600;
|
||||
word-break: break-all;
|
||||
margin: 0 0 0.5rem;
|
||||
font-weight: 600;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: #718096;
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.upload-button {
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background-color: #4299e1;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background-color: #4299e1;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.upload-button:hover {
|
||||
background-color: #3182ce;
|
||||
background-color: #3182ce;
|
||||
}
|
||||
|
||||
.upload-button:disabled {
|
||||
background-color: #cbd5e0;
|
||||
cursor: not-allowed;
|
||||
background-color: #cbd5e0;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.upload-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem;
|
||||
background-color: #ebf8ff;
|
||||
border-radius: 6px;
|
||||
color: #2c5282;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem;
|
||||
background-color: #ebf8ff;
|
||||
border-radius: 6px;
|
||||
color: #2c5282;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid #bee3f8;
|
||||
border-top-color: #4299e1;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid #bee3f8;
|
||||
border-top-color: #4299e1;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.success-message {
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem;
|
||||
background-color: #c6f6d5;
|
||||
color: #22543d;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem;
|
||||
background-color: #c6f6d5;
|
||||
color: #22543d;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem;
|
||||
background-color: #fed7d7;
|
||||
color: #742a2a;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem;
|
||||
background-color: #fed7d7;
|
||||
color: #742a2a;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="upload-widget">
|
||||
<div class="upload-area">
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept=".csv"
|
||||
@change="handleFileSelect"
|
||||
:disabled="isUploading"
|
||||
class="file-input"
|
||||
/>
|
||||
|
||||
<div class="upload-info">
|
||||
<div v-if="!selectedFile" class="placeholder">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="17 8 12 3 7 8"></polyline>
|
||||
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||||
</svg>
|
||||
<p>Select a CSV file to upload</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="file-selected">
|
||||
<p class="file-name">{{ selectedFile.name }}</p>
|
||||
<p class="file-size">{{ formatFileSize(selectedFile.size) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="upload-widget">
|
||||
<div class="upload-area">
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept=".csv"
|
||||
@change="handleFileSelect"
|
||||
:disabled="isUploading"
|
||||
class="file-input"
|
||||
/>
|
||||
|
||||
<button
|
||||
v-if="selectedFile && !isUploading"
|
||||
@click="uploadFile"
|
||||
class="upload-button"
|
||||
:disabled="isUploading"
|
||||
>
|
||||
Upload File
|
||||
</button>
|
||||
<div class="upload-info">
|
||||
<div v-if="!selectedFile" class="placeholder">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="17 8 12 3 7 8"></polyline>
|
||||
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||||
</svg>
|
||||
<p>Select a CSV file to upload</p>
|
||||
</div>
|
||||
|
||||
<div v-if="isUploading" class="upload-status">
|
||||
<div class="spinner"></div>
|
||||
<p>Uploading...</p>
|
||||
</div>
|
||||
<div v-else class="file-selected">
|
||||
<p class="file-name">{{ selectedFile.name }}</p>
|
||||
<p class="file-size">{{ formatFileSize(selectedFile.size) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="uploadSuccess" class="success-message">
|
||||
✓ File uploaded successfully!
|
||||
</div>
|
||||
<button
|
||||
v-if="selectedFile && !isUploading"
|
||||
@click="uploadFile"
|
||||
class="upload-button"
|
||||
:disabled="isUploading"
|
||||
>
|
||||
Upload File
|
||||
</button>
|
||||
|
||||
<div v-if="errorMessage" class="error-message">
|
||||
✗ {{ errorMessage }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isUploading" class="upload-status">
|
||||
<div class="spinner"></div>
|
||||
<p>Uploading...</p>
|
||||
</div>
|
||||
|
||||
<div v-if="uploadSuccess" class="success-message">
|
||||
✓ File uploaded successfully!
|
||||
</div>
|
||||
|
||||
<div v-if="errorMessage" class="error-message">✗ {{ errorMessage }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { ref } from "vue";
|
||||
|
||||
interface Props {
|
||||
uploadUrl: string;
|
||||
headers?: Record<string, string>;
|
||||
additionalData?: Record<string, string>;
|
||||
uploadUrl: string;
|
||||
headers?: Record<string, string>;
|
||||
additionalData?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'doError', error: Error): void;
|
||||
(e: 'doFileSelected', file: File): void;
|
||||
(e: 'doSuccess', response: any): void;
|
||||
(e: "doError", error: Error): void;
|
||||
(e: "doFileSelected", file: File): void;
|
||||
(e: "doSuccess", response: any): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
headers: () => ({}),
|
||||
additionalData: () => ({})
|
||||
headers: () => ({}),
|
||||
additionalData: () => ({}),
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
|
@ -212,89 +223,89 @@ const fileInput = ref<HTMLInputElement | null>(null);
|
|||
const selectedFile = ref<File | null>(null);
|
||||
const isUploading = ref(false);
|
||||
const uploadSuccess = ref(false);
|
||||
const errorMessage = ref('');
|
||||
const errorMessage = ref("");
|
||||
|
||||
const handleFileSelect = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const file = target.files?.[0];
|
||||
|
||||
if (file) {
|
||||
// Validate it's a CSV file
|
||||
if (!file.name.toLowerCase().endsWith('.csv')) {
|
||||
errorMessage.value = 'Please select a valid CSV file';
|
||||
selectedFile.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
selectedFile.value = file;
|
||||
errorMessage.value = '';
|
||||
uploadSuccess.value = false;
|
||||
emit('doFileSelected', file);
|
||||
}
|
||||
const target = event.target as HTMLInputElement;
|
||||
const file = target.files?.[0];
|
||||
|
||||
if (file) {
|
||||
// Validate it's a CSV file
|
||||
if (!file.name.toLowerCase().endsWith(".csv")) {
|
||||
errorMessage.value = "Please select a valid CSV file";
|
||||
selectedFile.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
selectedFile.value = file;
|
||||
errorMessage.value = "";
|
||||
uploadSuccess.value = false;
|
||||
emit("doFileSelected", file);
|
||||
}
|
||||
};
|
||||
|
||||
const uploadFile = async () => {
|
||||
if (!selectedFile.value) return;
|
||||
if (!selectedFile.value) return;
|
||||
|
||||
isUploading.value = true;
|
||||
errorMessage.value = '';
|
||||
uploadSuccess.value = false;
|
||||
isUploading.value = true;
|
||||
errorMessage.value = "";
|
||||
uploadSuccess.value = false;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', selectedFile.value);
|
||||
|
||||
// Add any additional data to the form
|
||||
Object.entries(props.additionalData).forEach(([key, value]) => {
|
||||
formData.append(key, value);
|
||||
});
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("file", selectedFile.value);
|
||||
|
||||
const response = await fetch(props.uploadUrl, {
|
||||
body: formData,
|
||||
headers: {
|
||||
...props.headers,
|
||||
// Don't set Content-Type - let browser set it with boundary
|
||||
},
|
||||
method: 'POST',
|
||||
});
|
||||
// Add any additional data to the form
|
||||
Object.entries(props.additionalData).forEach(([key, value]) => {
|
||||
formData.append(key, value);
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Upload failed: ${response.statusText}`);
|
||||
}
|
||||
const response = await fetch(props.uploadUrl, {
|
||||
body: formData,
|
||||
headers: {
|
||||
...props.headers,
|
||||
// Don't set Content-Type - let browser set it with boundary
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
uploadSuccess.value = true;
|
||||
emit('doSuccess', data);
|
||||
|
||||
resetUpload();
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : 'Upload failed';
|
||||
emit('doError', error as Error);
|
||||
} finally {
|
||||
isUploading.value = false;
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`Upload failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
uploadSuccess.value = true;
|
||||
emit("doSuccess", data);
|
||||
|
||||
resetUpload();
|
||||
} catch (error) {
|
||||
errorMessage.value =
|
||||
error instanceof Error ? error.message : "Upload failed";
|
||||
emit("doError", error as Error);
|
||||
} finally {
|
||||
isUploading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const resetUpload = () => {
|
||||
selectedFile.value = null;
|
||||
uploadSuccess.value = false;
|
||||
errorMessage.value = '';
|
||||
if (fileInput.value) {
|
||||
fileInput.value.value = '';
|
||||
}
|
||||
selectedFile.value = null;
|
||||
uploadSuccess.value = false;
|
||||
errorMessage.value = "";
|
||||
if (fileInput.value) {
|
||||
fileInput.value.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
const k = 1024;
|
||||
const sizes = ["Bytes", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
|
||||
};
|
||||
|
||||
// Expose methods for parent component
|
||||
defineExpose({
|
||||
resetUpload
|
||||
resetUpload,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,10 @@
|
|||
</div>
|
||||
|
||||
<div v-if="selectedCommunication" class="h-100 d-flex flex-column">
|
||||
<PublicreportCard :report="selectedCommunication.public_report" @viewImage="openPhotoViewer" />
|
||||
<PublicreportCard
|
||||
:report="selectedCommunication.public_report"
|
||||
@viewImage="openPhotoViewer"
|
||||
/>
|
||||
<!-- Report Details -->
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
<template>
|
||||
<p>A flyover pool</p>
|
||||
<MapProxiedArcgisTile
|
||||
id="tile-map"
|
||||
:latitude="pool.location.latitude"
|
||||
:longitude="pool.location.longitude"
|
||||
:markers="tileMapMarkers"
|
||||
:organizationId="user.organization.id"
|
||||
:tegola="user.urls.tegola" />
|
||||
id="tile-map"
|
||||
:latitude="pool.location.latitude"
|
||||
:longitude="pool.location.longitude"
|
||||
:markers="tileMapMarkers"
|
||||
:organizationId="user.organization.id"
|
||||
:tegola="user.urls.tegola"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
|
|||
|
|
@ -15,18 +15,13 @@
|
|||
</div>
|
||||
<div v-else>
|
||||
<h1>Map failed to load</h1>
|
||||
<p>{{error}}</p>
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
import {
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
ref,
|
||||
watch,
|
||||
} from "vue";
|
||||
import { onMounted, onUnmounted, ref, watch } from "vue";
|
||||
import { Bounds, Marker } from "@/types";
|
||||
import maplibregl from "maplibre-gl";
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
</div>
|
||||
<div v-else>
|
||||
<h1>Map failed to load</h1>
|
||||
<p>{{error}}</p>
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ import { Point } from "@/types";
|
|||
import maplibregl from "maplibre-gl";
|
||||
|
||||
interface Emits {
|
||||
(e: "map-click", latitude: Number, longitude: Number): void
|
||||
(e: "map-click", latitude: Number, longitude: Number): void;
|
||||
}
|
||||
interface Props {
|
||||
location: Point;
|
||||
|
|
@ -40,8 +40,8 @@ const props = defineProps<Props>();
|
|||
const error = ref<string | null>(null);
|
||||
const mapContainer = ref<HTMLElement | null>(null);
|
||||
const map = ref<maplibregl.Map | null>(null);
|
||||
const markerInstances = ref<Map<string, maplibrgl.Marker>>(new Map())
|
||||
const markers = ref<Map<string, maplibrgl.Marker>>(new Map())
|
||||
const markerInstances = ref<Map<string, maplibrgl.Marker>>(new Map());
|
||||
const markers = ref<Map<string, maplibrgl.Marker>>(new Map());
|
||||
|
||||
// Watch for latitude/longitude changes
|
||||
watch(
|
||||
|
|
@ -108,7 +108,7 @@ const initializeMap = () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
console.error("hey dummy", e);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -47,13 +47,22 @@
|
|||
|
||||
<div class="mb-3">
|
||||
<div class="text-muted small mb-2">Lead → Field Assignment</div>
|
||||
<button class="btn btn-outline-success tool-button" @click="emit('doCreateProposedAssignment')">
|
||||
<button
|
||||
class="btn btn-outline-success tool-button"
|
||||
@click="emit('doCreateProposedAssignment')"
|
||||
>
|
||||
Create Proposed Assignment
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary tool-button" @click="emit('doAddLeadsToAssignment')">
|
||||
<button
|
||||
class="btn btn-outline-secondary tool-button"
|
||||
@click="emit('doAddLeadsToAssignment')"
|
||||
>
|
||||
Add Leads to Existing Assignment
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary tool-button" @click="emit('doSplitLead')">
|
||||
<button
|
||||
class="btn btn-outline-secondary tool-button"
|
||||
@click="emit('doSplitLead')"
|
||||
>
|
||||
Split Lead
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -62,11 +71,22 @@
|
|||
|
||||
<div class="mb-3">
|
||||
<div class="text-muted small mb-2">Assignment Controls</div>
|
||||
<button class="btn btn-outline-dark tool-button" @click="emit('doSetPriority')">Set Priority</button>
|
||||
<button class="btn btn-outline-dark tool-button" @click="emit('doEstimateEffort')">
|
||||
<button
|
||||
class="btn btn-outline-dark tool-button"
|
||||
@click="emit('doSetPriority')"
|
||||
>
|
||||
Set Priority
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-outline-dark tool-button"
|
||||
@click="emit('doEstimateEffort')"
|
||||
>
|
||||
Estimate Effort
|
||||
</button>
|
||||
<button class="btn btn-outline-dark tool-button" @click="emit('doSendToOperations')">
|
||||
<button
|
||||
class="btn btn-outline-dark tool-button"
|
||||
@click="emit('doSendToOperations')"
|
||||
>
|
||||
Send to Operations
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -42,13 +42,10 @@
|
|||
Click signals from the left panel to select them
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-2"
|
||||
v-show="selectedSignals.length > 0"
|
||||
>
|
||||
<div v-for="signal in selectedSignals" :key="signal.id">
|
||||
<PlanningColumnDetailEntry :signal="signal"/>
|
||||
</div>
|
||||
<div class="mt-2" v-show="selectedSignals.length > 0">
|
||||
<div v-for="signal in selectedSignals" :key="signal.id">
|
||||
<PlanningColumnDetailEntry :signal="signal" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -123,18 +120,24 @@ const selectedSignalLocation = () => {
|
|||
return accumulator;
|
||||
}, null);
|
||||
const loc = first_pool?.location;
|
||||
return loc || {
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
}
|
||||
return (
|
||||
loc || {
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
}
|
||||
);
|
||||
};
|
||||
const showMapTile = () => {
|
||||
return selectedSignalLocation() && props.selectedSignals.value
|
||||
.values()
|
||||
.reduce(
|
||||
(accumulator, current) => accumulator || current.type === "flyover pool",
|
||||
false,
|
||||
);
|
||||
return (
|
||||
selectedSignalLocation() &&
|
||||
props.selectedSignals.value
|
||||
.values()
|
||||
.reduce(
|
||||
(accumulator, current) =>
|
||||
accumulator || current.type === "flyover pool",
|
||||
false,
|
||||
)
|
||||
);
|
||||
};
|
||||
const updateSignalLocation = (event) => {
|
||||
const signalId = event.detail.signalId;
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
<TimeRelative :time="signal.created"></TimeRelative>
|
||||
<p>{{ shortAddress(signal.address) }}</p>
|
||||
<div v-if="signal.type == 'flyover pool' && signal.pool">
|
||||
<FlyoverPoolCard :pool="signal.pool"/>
|
||||
<FlyoverPoolCard :pool="signal.pool" />
|
||||
</div>
|
||||
<div v-else-if="signal.type == 'publicreport nuisance'">
|
||||
<PublicreportCard :report="signal.report"/>
|
||||
<PublicreportCard :report="signal.report" />
|
||||
</div>
|
||||
<div v-else-if="signal.type == 'publicreport water'">
|
||||
<PublicreportCard :report="signal.report"/>
|
||||
<PublicreportCard :report="signal.report" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -138,7 +138,10 @@
|
|||
:class="{ selected: isSelected(signal.id) }"
|
||||
@click="toggleSignal(signal)"
|
||||
>
|
||||
<PlanningColumnListEntry :selected="selectedSignalIDs.has(signal.id)" :signal="signal"/>
|
||||
<PlanningColumnListEntry
|
||||
:selected="selectedSignalIDs.has(signal.id)"
|
||||
:signal="signal"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import { shortAddress } from "../format";
|
|||
interface Props {
|
||||
selected: boolean;
|
||||
signal: Signal;
|
||||
};
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
function icon(signal: Signal): string {
|
||||
if (signal.type == "flyover pool") {
|
||||
|
|
@ -34,7 +34,7 @@ function icon(signal: Signal): string {
|
|||
return "bi-mosquito";
|
||||
} else if (signal.type == "publicreport water") {
|
||||
return "bi-water";
|
||||
} else {
|
||||
} else {
|
||||
return "bi-mosquito";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,24 +16,16 @@
|
|||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<div>
|
||||
<h5 class="mb-1">
|
||||
<span
|
||||
v-if="
|
||||
report.type === 'nuisance'
|
||||
"
|
||||
>
|
||||
<span v-if="report.type === 'nuisance'">
|
||||
<i class="bi bi-mosquito icon-nuisance"></i>
|
||||
Nuisance Report
|
||||
</span>
|
||||
<span
|
||||
v-if="report.type === 'water'"
|
||||
>
|
||||
<span v-if="report.type === 'water'">
|
||||
<i class="bi bi-droplet-fill icon-standing-water"></i>
|
||||
Standing Water Report
|
||||
</span>
|
||||
</h5>
|
||||
<small class="text-muted"
|
||||
>Report ID: #{{ report.public_id }}</small
|
||||
>
|
||||
<small class="text-muted">Report ID: #{{ report.public_id }}</small>
|
||||
</div>
|
||||
<span class="badge bg-secondary">
|
||||
<TimeRelative :time="report.created" />
|
||||
|
|
@ -49,11 +41,7 @@
|
|||
<i class="bi bi-geo-alt"></i> Address
|
||||
</label>
|
||||
<div class="fw-medium">
|
||||
{{
|
||||
formatAddress(
|
||||
report.address,
|
||||
)
|
||||
}}
|
||||
{{ formatAddress(report.address) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
|
|
@ -61,25 +49,18 @@
|
|||
<i class="bi bi-person"></i> Reporter Name
|
||||
</label>
|
||||
<div class="fw-medium">
|
||||
{{
|
||||
report.reporter.name ||
|
||||
"not given"
|
||||
}}
|
||||
{{ report.reporter.name || "not given" }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label
|
||||
v-if="
|
||||
report.reporter.has_email
|
||||
"
|
||||
v-if="report.reporter.has_email"
|
||||
class="form-label text-muted small mb-0"
|
||||
>
|
||||
<i class="bi bi-envelope"></i>
|
||||
</label>
|
||||
<label
|
||||
v-if="
|
||||
report.reporter.has_phone
|
||||
"
|
||||
v-if="report.reporter.has_phone"
|
||||
class="form-label text-muted small mb-0"
|
||||
>
|
||||
<i class="bi bi-phone"></i>
|
||||
|
|
@ -126,9 +107,7 @@
|
|||
<div>
|
||||
<ul>
|
||||
<li v-if="report.nuisance?.is_location_backyard">Backyard</li>
|
||||
<li v-if="report.nuisance?.is_location_frontyard">
|
||||
Frontyard
|
||||
</li>
|
||||
<li v-if="report.nuisance?.is_location_frontyard">Frontyard</li>
|
||||
<li v-if="report.nuisance?.is_location_garden">Garden</li>
|
||||
<li v-if="report.nuisance?.is_location_other">Other</li>
|
||||
<li v-if="report.nuisance?.is_location_pool">Pool</li>
|
||||
|
|
@ -245,9 +224,7 @@
|
|||
>
|
||||
<i
|
||||
class="bi"
|
||||
:class="
|
||||
report.water?.has_pupae ? 'bi-check-circle' : 'bi-circle'
|
||||
"
|
||||
:class="report.water?.has_pupae ? 'bi-check-circle' : 'bi-circle'"
|
||||
></i>
|
||||
Pupae
|
||||
</span>
|
||||
|
|
@ -259,9 +236,7 @@
|
|||
>
|
||||
<i
|
||||
class="bi"
|
||||
:class="
|
||||
report.water?.has_adult ? 'bi-check-circle' : 'bi-circle'
|
||||
"
|
||||
:class="report.water?.has_adult ? 'bi-check-circle' : 'bi-circle'"
|
||||
></i>
|
||||
Adult Mosquitoes
|
||||
</span>
|
||||
|
|
@ -311,10 +286,7 @@
|
|||
</div>
|
||||
<div class="card-body">
|
||||
<div
|
||||
v-if="
|
||||
report.images &&
|
||||
report.images.length > 0
|
||||
"
|
||||
v-if="report.images && report.images.length > 0"
|
||||
class="d-flex flex-wrap gap-2"
|
||||
>
|
||||
<img
|
||||
|
|
@ -327,10 +299,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
!report.images ||
|
||||
report.images.length === 0
|
||||
"
|
||||
v-if="!report.images || report.images.length === 0"
|
||||
class="text-muted text-center py-3"
|
||||
>
|
||||
<i class="bi bi-camera-slash fs-4"></i>
|
||||
|
|
|
|||
43
ts/router.ts
43
ts/router.ts
|
|
@ -154,11 +154,11 @@ const routes: RouteRecordRaw[] = [
|
|||
meta: { requiresAuth: true, showSidebar: true },
|
||||
},
|
||||
// Catch-all route - must be last
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
component: NotFound
|
||||
}
|
||||
{
|
||||
path: "/:pathMatch(.*)*",
|
||||
name: "NotFound",
|
||||
component: NotFound,
|
||||
},
|
||||
];
|
||||
|
||||
export const router = createRouter({
|
||||
|
|
@ -168,23 +168,22 @@ export const router = createRouter({
|
|||
|
||||
// Global navigation guard
|
||||
router.beforeEach(async (to, from) => {
|
||||
const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
|
||||
|
||||
if (requiresAuth) {
|
||||
try {
|
||||
// Check if user is authenticated (could be an API call)
|
||||
const isAuthenticated = await apiClient.isAuthenticated();
|
||||
if (!isAuthenticated) {
|
||||
return '/signin';
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("check auth failed");
|
||||
return '/signin';
|
||||
}
|
||||
}
|
||||
const requiresAuth = to.matched.some((record) => record.meta.requiresAuth);
|
||||
|
||||
if (requiresAuth) {
|
||||
try {
|
||||
// Check if user is authenticated (could be an API call)
|
||||
const isAuthenticated = await apiClient.isAuthenticated();
|
||||
if (!isAuthenticated) {
|
||||
return "/signin";
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("check auth failed");
|
||||
return "/signin";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -271,7 +271,7 @@ function updateMap() {
|
|||
location: {
|
||||
lng: loc.longitude,
|
||||
lat: loc.latitude,
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
console.log("markers now", mapMarkers.value);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
<template>
|
||||
<p>No idea where you wanted to go with that one.</p>
|
||||
<p>No idea where you wanted to go with that one.</p>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,23 +1,23 @@
|
|||
<style scoped>
|
||||
.login-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.login-box {
|
||||
box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.login-form-section {
|
||||
padding: 40px;
|
||||
}
|
||||
.product-info-section {
|
||||
padding: 40px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.login-header {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.login-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.login-box {
|
||||
box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.login-form-section {
|
||||
padding: 40px;
|
||||
}
|
||||
.product-info-section {
|
||||
padding: 40px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.login-header {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
|
|
@ -61,7 +61,6 @@
|
|||
</div>
|
||||
-->
|
||||
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary">Login</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -167,10 +167,7 @@
|
|||
available to Nidus
|
||||
</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<a
|
||||
class="btn btn-outline-warning"
|
||||
href="/configuration/upload"
|
||||
>
|
||||
<a class="btn btn-outline-warning" href="/configuration/upload">
|
||||
Manage Uploads
|
||||
<i class="bi bi-arrow-right ms-1"></i>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -49,8 +49,8 @@
|
|||
</p>
|
||||
<RouterLink to="/configuration/upload/pool">
|
||||
<button class="btn btn-primary">
|
||||
<i class="bi bi-upload me-2"></i>Upload Green Pool Data</button
|
||||
>
|
||||
<i class="bi bi-upload me-2"></i>Upload Green Pool Data
|
||||
</button>
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="card-footer bg-white text-muted">
|
||||
|
|
@ -134,24 +134,24 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="upload in uploads">
|
||||
<td><TimeRelative :time="upload.created"/></td>
|
||||
<td>{{upload.type}}</td>
|
||||
<td>{{upload.filename}}</td>
|
||||
<td>
|
||||
<span class="badge" :class="upload.status"
|
||||
>{{upload.status}}</span
|
||||
>
|
||||
</td>
|
||||
<td>{{upload.record_count}} entries</td>
|
||||
<td>
|
||||
<a
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
:href="`/configuration/upload/${upload.id}`"
|
||||
>View</a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-for="upload in uploads">
|
||||
<td><TimeRelative :time="upload.created" /></td>
|
||||
<td>{{ upload.type }}</td>
|
||||
<td>{{ upload.filename }}</td>
|
||||
<td>
|
||||
<span class="badge" :class="upload.status">{{
|
||||
upload.status
|
||||
}}</span>
|
||||
</td>
|
||||
<td>{{ upload.record_count }} entries</td>
|
||||
<td>
|
||||
<a
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
:href="`/configuration/upload/${upload.id}`"
|
||||
>View</a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
<style scoped>
|
||||
.upload-card {
|
||||
transition:
|
||||
transform 0.2s,
|
||||
box-shadow 0.2s;
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
}
|
||||
.upload-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.card-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
<style scoped>
|
||||
.upload-card {
|
||||
transition:
|
||||
transform 0.2s,
|
||||
box-shadow 0.2s;
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
}
|
||||
.upload-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.card-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="container py-5">
|
||||
<div class="row mb-4">
|
||||
|
|
@ -50,12 +50,9 @@
|
|||
<i class="bi bi-filetype-csv me-1"></i> CSV Format
|
||||
</span>
|
||||
</div>
|
||||
<RouterLink
|
||||
to="/configuration/upload/pool/flyover"
|
||||
>
|
||||
<button
|
||||
class="btn btn-primary btn-lg w-100">
|
||||
<i class="bi bi-upload me-2"></i>Let's do this
|
||||
<RouterLink to="/configuration/upload/pool/flyover">
|
||||
<button class="btn btn-primary btn-lg w-100">
|
||||
<i class="bi bi-upload me-2"></i>Let's do this
|
||||
</button>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
|
@ -85,11 +82,8 @@
|
|||
<i class="bi bi-filetype-csv me-1"></i> CSV Format
|
||||
</span>
|
||||
</div>
|
||||
<RouterLink
|
||||
to="/configuration/upload/pool/custom"
|
||||
>
|
||||
<button
|
||||
class="btn btn-success btn-lg w-100">
|
||||
<RouterLink to="/configuration/upload/pool/custom">
|
||||
<button class="btn btn-success btn-lg w-100">
|
||||
<i class="bi bi-upload me-2"></i>Pick me
|
||||
</button>
|
||||
</RouterLink>
|
||||
|
|
|
|||
|
|
@ -82,10 +82,11 @@
|
|||
</div>
|
||||
<div class="card-body">
|
||||
<CSVUpload
|
||||
upload-url="/api/upload/pool/flyover"
|
||||
upload-url="/api/upload/pool/flyover"
|
||||
@doError="onError"
|
||||
@doFileSelected="onFileSelected"
|
||||
@doSuccess="onUploadSuccess" />
|
||||
@doSuccess="onUploadSuccess"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
127
vite.config.ts
127
vite.config.ts
|
|
@ -3,69 +3,74 @@ import vue from "@vitejs/plugin-vue";
|
|||
import path from "path";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
plugins: [vue()],
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./ts"),
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./ts"),
|
||||
},
|
||||
},
|
||||
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
api: "modern-compiler",
|
||||
silenceDeprecations: ["import", "global-builtin", "if-function", "color-functions"],
|
||||
},
|
||||
},
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
api: "modern-compiler",
|
||||
silenceDeprecations: [
|
||||
"import",
|
||||
"global-builtin",
|
||||
"if-function",
|
||||
"color-functions",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
build: {
|
||||
manifest: true,
|
||||
outDir: "static/gen",
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: path.resolve(__dirname, "ts/main.ts"),
|
||||
},
|
||||
output: {
|
||||
entryFileNames: "js/bundle.[hash].js",
|
||||
chunkFileNames: "js/[name].[hash].js",
|
||||
assetFileNames: (assetInfo) => {
|
||||
if (/\.(woff2?|ttf|eot)$/.test(assetInfo.name || "")) {
|
||||
return "fonts/[name].[hash][extname]";
|
||||
}
|
||||
if (/\.css$/.test(assetInfo.name || "")) {
|
||||
return "css/style.[hash][extname]";
|
||||
}
|
||||
return "assets/[name].[hash][extname]";
|
||||
},
|
||||
},
|
||||
},
|
||||
sourcemap: true,
|
||||
},
|
||||
build: {
|
||||
manifest: true,
|
||||
outDir: "static/gen",
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: path.resolve(__dirname, "ts/main.ts"),
|
||||
},
|
||||
output: {
|
||||
entryFileNames: "js/bundle.[hash].js",
|
||||
chunkFileNames: "js/[name].[hash].js",
|
||||
assetFileNames: (assetInfo) => {
|
||||
if (/\.(woff2?|ttf|eot)$/.test(assetInfo.name || "")) {
|
||||
return "fonts/[name].[hash][extname]";
|
||||
}
|
||||
if (/\.css$/.test(assetInfo.name || "")) {
|
||||
return "css/style.[hash][extname]";
|
||||
}
|
||||
return "assets/[name].[hash][extname]";
|
||||
},
|
||||
},
|
||||
},
|
||||
sourcemap: true,
|
||||
},
|
||||
|
||||
server: {
|
||||
allowedHosts: ["poweredge.local", "dev-sync.nidus.cloud"],
|
||||
port: 9000,
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://127.0.0.1:9002",
|
||||
changeOrigin: false,
|
||||
},
|
||||
"/configuration/upload/pool/flyover": {
|
||||
target: "http://127.0.0.1:9002",
|
||||
changeOrigin: false,
|
||||
},
|
||||
"/signin": {
|
||||
target: "http://localhost:9002",
|
||||
changeOrigin: false,
|
||||
},
|
||||
"/signup": {
|
||||
target: "http://localhost:9002",
|
||||
changeOrigin: false,
|
||||
}
|
||||
},
|
||||
strictPort: true,
|
||||
},
|
||||
server: {
|
||||
allowedHosts: ["poweredge.local", "dev-sync.nidus.cloud"],
|
||||
port: 9000,
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://127.0.0.1:9002",
|
||||
changeOrigin: false,
|
||||
},
|
||||
"/configuration/upload/pool/flyover": {
|
||||
target: "http://127.0.0.1:9002",
|
||||
changeOrigin: false,
|
||||
},
|
||||
"/signin": {
|
||||
target: "http://localhost:9002",
|
||||
changeOrigin: false,
|
||||
},
|
||||
"/signup": {
|
||||
target: "http://localhost:9002",
|
||||
changeOrigin: false,
|
||||
},
|
||||
},
|
||||
strictPort: true,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue