feat: auto-odabir i datum u dropdownu putnih naloga za taskove
- TaskCreateModal: auto-pretselektira jedini otvoreni nalog za danas pri otvaranju modala - Dropdown labele prikazuju datum naloga i oznacavaju danasnji nalog sa zvjezdicom (★) - Dodana napomena da vise taskova moze dijeliti isti putni nalog - TaskServiceRecordsModal: danasnji nalozi sortirani na vrh liste - Datum prikazan u dropdownu i u prikazu trenutno odabranog naloga Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
218
docs/production-db-local-setup.md
Normal file
218
docs/production-db-local-setup.md
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
# Preuzimanje produkcijske baze podataka na lokalni PC
|
||||||
|
|
||||||
|
Vodič za lokalni razvoj s kopijom produkcijskog PostgreSQL-a bez mijenjanja produkcijske baze.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Preduvjeti
|
||||||
|
|
||||||
|
- SSH pristup produkcijskom serveru
|
||||||
|
- Docker Desktop instaliran lokalno
|
||||||
|
- `scp` dostupan u PowerShell / terminalu
|
||||||
|
- Lokalni `.env` s `DEBUG=True` i **drugačijim** `SECRET_KEY` od produkcije
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Korak 1 — Na produkcijskom serveru: napravi dump
|
||||||
|
|
||||||
|
SSH-aj na produkcijski server, zatim pokreni:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec 004erpdb pg_dump \
|
||||||
|
-U $POSTGRES_USER \
|
||||||
|
-d $POSTGRES_DB \
|
||||||
|
--no-owner \
|
||||||
|
--no-acl \
|
||||||
|
-Fc \
|
||||||
|
-f /tmp/prod_dump.dump
|
||||||
|
```
|
||||||
|
|
||||||
|
> **`-Fc`** — custom format (komprimiran, pogodniji za `pg_restore`)
|
||||||
|
> **`--no-owner --no-acl`** — izostavlja produkcijske usere i privilegije
|
||||||
|
|
||||||
|
Ako env varijable nisu dostupne u shell sesiji, napiši vrijednosti direktno:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec 004erpdb sh -c \
|
||||||
|
"pg_dump -U TVOJ_USER TVOJA_BAZA --no-owner --no-acl -Fc -f /tmp/prod_dump.dump"
|
||||||
|
```
|
||||||
|
|
||||||
|
Ako baza ima desetke gigabajta, pg_dump može opteretiti disk produkcije. U tom slučaju, dumpaj u komprimirani format **--exclude-table-data='log_*'**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec 004erpdb pg_dump \
|
||||||
|
-U $POSTGRES_USER \
|
||||||
|
-d $POSTGRES_DB \
|
||||||
|
--no-owner \
|
||||||
|
--no-acl \
|
||||||
|
--exclude-table-data='log_*' \
|
||||||
|
-Fc \
|
||||||
|
-f /tmp/prod_dump.dump
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Korak 2 — Kopiraj dump na lokalni PC
|
||||||
|
|
||||||
|
Iz lokalnog PowerShell terminala:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
scp tvoj_user@IP_SERVERA:/tmp/prod_dump.dump C:\Users\mario.tkalac\Downloads\prod_dump.dump
|
||||||
|
```
|
||||||
|
|
||||||
|
S SSH ključem:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
scp -i C:\Users\mario.tkalac\.ssh\id_rsa `
|
||||||
|
tvoj_user@IP_SERVERA:/tmp/prod_dump.dump `
|
||||||
|
C:\Users\mario.tkalac\Downloads\prod_dump.dump
|
||||||
|
```
|
||||||
|
|
||||||
|
Brisanje dump-a s produkcijskog servera (opcionalno):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
ssh tvoj_user@IP_SERVERA "rm /tmp/prod_dump.dump"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Korak 3 — Pokreni lokalnu bazu
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd C:\Users\mario.tkalac\workspace\aplikacija\004.erp.generic
|
||||||
|
|
||||||
|
docker compose -f docker-compose.yml up -d db
|
||||||
|
```
|
||||||
|
|
||||||
|
Pričekaj dok container postane `healthy`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker ps --filter "name=004erpdb"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Korak 4 — Restoriraj dump u lokalnu bazu
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Kopiraj dump file u container
|
||||||
|
docker cp C:\Users\mario.tkalac\Downloads\prod_dump.dump 004erpdb:/tmp/prod_dump.dump
|
||||||
|
|
||||||
|
# Dropaj lokalnu bazu i kreiraj novu praznu (BRIŠE lokalne podatke!)
|
||||||
|
docker exec 004erpdb dropdb -U TVOJ_USER TVOJA_BAZA --if-exists
|
||||||
|
docker exec 004erpdb createdb -U TVOJ_USER TVOJA_BAZA
|
||||||
|
|
||||||
|
# Restoriraj
|
||||||
|
docker exec 004erpdb pg_restore `
|
||||||
|
-U TVOJ_USER `
|
||||||
|
-d TVOJA_BAZA `
|
||||||
|
--no-owner `
|
||||||
|
--no-acl `
|
||||||
|
-Fc `
|
||||||
|
/tmp/prod_dump.dump
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ Zamijeni `TVOJ_USER` i `TVOJA_BAZA` s vrijednostima iz lokalnog `.env` fajla:
|
||||||
|
> `POSTGRES_USER` i `POSTGRES_DB`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Korak 5 — Primijeni nove migracije
|
||||||
|
|
||||||
|
Pokrenuti odmah nakon restore-a kako bi lokalna shema bila u sinkronizaciji s kodom:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml `
|
||||||
|
run --rm backend python manage.py migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
Migracije na postojećim podacima su sigurne — dodaju tablice/stupce, ne brišu podatke.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Korak 6 — Postavi lokalnog admin korisnika
|
||||||
|
|
||||||
|
Produkcijske lozinke su hashirane i ne mogu se koristiti direktno. Kreiraj novog superusera:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml `
|
||||||
|
run --rm backend python manage.py createsuperuser
|
||||||
|
```
|
||||||
|
|
||||||
|
Ili resetiraj lozinku postojećem accountu:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml `
|
||||||
|
run --rm backend python manage.py changepassword tvoj@email.com
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Korak 7 — Pokreni cijeli dev stack
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d backend frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
Provjeri:
|
||||||
|
- Backend: `http://localhost:8001/api/`
|
||||||
|
- Frontend: `http://localhost:4321/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Napomene o sigurnosti
|
||||||
|
|
||||||
|
| Tema | Akcija |
|
||||||
|
|------|--------|
|
||||||
|
| **Produkcijska baza** | `pg_dump` je read-only operacija — produkcijska baza nije dirnuta |
|
||||||
|
| **SECRET_KEY** | Lokalni `.env` **mora** imati drugačiji `SECRET_KEY` od produkcije |
|
||||||
|
| **DEBUG** | Lokalni `.env` mora imati `DEBUG=True` |
|
||||||
|
| **Media fajlovi** | Dump ne uključuje fajlove iz `media_volume`; kopirati ručno ako su potrebni |
|
||||||
|
| **Osobni podaci** | Baza može sadržavati osobne podatke klijenata — koristiti samo na zaštićenom lokalnom stroju |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Media fajlovi (opcionalno)
|
||||||
|
|
||||||
|
Ako trebaju priloženi fajlovi (slike, PDF-ovi):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Na produkcijskom serveru — arhiviraj media
|
||||||
|
docker run --rm \
|
||||||
|
-v media_volume:/data \
|
||||||
|
alpine tar czf /tmp/media.tar.gz -C /data .
|
||||||
|
|
||||||
|
# Kopiraj lokalno
|
||||||
|
scp tvoj_user@IP_SERVERA:/tmp/media.tar.gz C:\Users\mario.tkalac\Downloads\media.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
Raspakiraj u lokalni Docker volume:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Kopiraj arhivu u privremeni container
|
||||||
|
docker run --rm `
|
||||||
|
-v 004erpgeneric_media_volume:/data `
|
||||||
|
-v C:\Users\mario.tkalac\Downloads:/backup `
|
||||||
|
alpine sh -c "tar xzf /backup/media.tar.gz -C /data"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Brzi pregled naredbi (cheat sheet)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Dump na serveru
|
||||||
|
docker exec 004erpdb pg_dump -U USER DB --no-owner --no-acl -Fc -f /tmp/prod_dump.dump
|
||||||
|
|
||||||
|
# 2. Kopiraj lokalno
|
||||||
|
scp user@IP:/tmp/prod_dump.dump ~/Downloads/prod_dump.dump
|
||||||
|
|
||||||
|
# 3. Restore lokalno
|
||||||
|
docker cp ~/Downloads/prod_dump.dump 004erpdb:/tmp/prod_dump.dump
|
||||||
|
docker exec 004erpdb dropdb -U USER DB --if-exists
|
||||||
|
docker exec 004erpdb createdb -U USER DB
|
||||||
|
docker exec 004erpdb pg_restore -U USER -d DB --no-owner --no-acl -Fc /tmp/prod_dump.dump
|
||||||
|
|
||||||
|
# 4. Migracije
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.dev.yml run --rm backend python manage.py migrate
|
||||||
|
```
|
||||||
@@ -13,6 +13,17 @@ const INITIAL_FORM = {
|
|||||||
scheduled_date: '',
|
scheduled_date: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function todayIso() {
|
||||||
|
return new Date().toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatWODate(dateStr) {
|
||||||
|
if (!dateStr) return '';
|
||||||
|
const d = new Date(dateStr);
|
||||||
|
if (Number.isNaN(d.getTime())) return '';
|
||||||
|
return d.toLocaleDateString('hr-HR');
|
||||||
|
}
|
||||||
|
|
||||||
export default function TaskCreateModal({ open, workOrders = [], contextCrane = null, onClose, onSuccess }) {
|
export default function TaskCreateModal({ open, workOrders = [], contextCrane = null, onClose, onSuccess }) {
|
||||||
const [form, setForm] = useState(INITIAL_FORM);
|
const [form, setForm] = useState(INITIAL_FORM);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
@@ -40,8 +51,18 @@ export default function TaskCreateModal({ open, workOrders = [], contextCrane =
|
|||||||
setTemplateId('');
|
setTemplateId('');
|
||||||
setTemplates([]);
|
setTemplates([]);
|
||||||
setTemplateLoading(false);
|
setTemplateLoading(false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}, [open]);
|
// Auto-pre-select today's open work order if exactly one exists for this crane
|
||||||
|
const today = todayIso();
|
||||||
|
const todayOpen = workOrders.filter(
|
||||||
|
(wo) => String(wo?.status || '').toLowerCase() !== 'closed'
|
||||||
|
&& String(wo?.date || '').slice(0, 10) === today
|
||||||
|
);
|
||||||
|
if (todayOpen.length === 1) {
|
||||||
|
setForm((prev) => ({ ...prev, work_order: String(todayOpen[0].id) }));
|
||||||
|
}
|
||||||
|
}, [open]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedTemplate) return;
|
if (!selectedTemplate) return;
|
||||||
@@ -254,12 +275,18 @@ export default function TaskCreateModal({ open, workOrders = [], contextCrane =
|
|||||||
className="rounded-lg border border-border-hairline bg-canvas-base px-3 py-2 text-text-main disabled:opacity-50"
|
className="rounded-lg border border-border-hairline bg-canvas-base px-3 py-2 text-text-main disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<option value="">— Bez povezanog naloga —</option>
|
<option value="">— Bez povezanog naloga —</option>
|
||||||
{availableWorkOrders.map((workOrder) => (
|
{availableWorkOrders.map((workOrder) => {
|
||||||
|
const isToday = String(workOrder.date || '').slice(0, 10) === todayIso();
|
||||||
|
return (
|
||||||
<option key={workOrder.id} value={String(workOrder.id)}>
|
<option key={workOrder.id} value={String(workOrder.id)}>
|
||||||
{formatWorkOrderDisplayCode(workOrder)} • {workOrder.craneLabel || workOrder.craneInfo?.registration_number || '-'} • {formatPurposeLabel(workOrder.purpose) || '-'}
|
{isToday ? '★ ' : ''}{formatWorkOrderDisplayCode(workOrder)} • {formatWODate(workOrder.date)} • {workOrder.craneLabel || workOrder.craneInfo?.registration_number || '-'} • {formatPurposeLabel(workOrder.purpose) || '-'}
|
||||||
</option>
|
</option>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</select>
|
</select>
|
||||||
|
<span className="text-[11px] text-text-muted">
|
||||||
|
Više zadataka može dijeliti isti putni nalog. Danas kreiran nalog označen je s ★.
|
||||||
|
</span>
|
||||||
{templateMode && (
|
{templateMode && (
|
||||||
<span className="text-[11px] text-text-muted">
|
<span className="text-[11px] text-text-muted">
|
||||||
Kod template unosa nalog nije obavezan i backend ga neće automatski povezati.
|
Kod template unosa nalog nije obavezan i backend ga neće automatski povezati.
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import { getTaskCraneDisplay, getTaskCraneOwner } from '../../lib/taskCraneDispl
|
|||||||
|
|
||||||
const STATUS_OPTIONS = ['aktivan', 'servis', 'spreman_za_zavrsetak', 'zavrsen', 'neaktivan'];
|
const STATUS_OPTIONS = ['aktivan', 'servis', 'spreman_za_zavrsetak', 'zavrsen', 'neaktivan'];
|
||||||
|
|
||||||
|
function todayIso() {
|
||||||
|
return new Date().toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
function formatDate(value) {
|
function formatDate(value) {
|
||||||
if (!value) return '-';
|
if (!value) return '-';
|
||||||
const parsed = new Date(value);
|
const parsed = new Date(value);
|
||||||
@@ -79,10 +83,15 @@ export default function TaskServiceRecordsModal({
|
|||||||
[workOrders, task?.work_order, selectedWorkOrderId]
|
[workOrders, task?.work_order, selectedWorkOrderId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const sortedWorkOrders = useMemo(
|
const sortedWorkOrders = useMemo(() => {
|
||||||
() => [...workOrders].sort((a, b) => String(b.date || '').localeCompare(String(a.date || ''))),
|
const today = todayIso();
|
||||||
[workOrders]
|
return [...workOrders].sort((a, b) => {
|
||||||
);
|
const aToday = String(a.date || '').slice(0, 10) === today ? 1 : 0;
|
||||||
|
const bToday = String(b.date || '').slice(0, 10) === today ? 1 : 0;
|
||||||
|
if (bToday !== aToday) return bToday - aToday;
|
||||||
|
return String(b.date || '').localeCompare(String(a.date || ''));
|
||||||
|
});
|
||||||
|
}, [workOrders]);
|
||||||
|
|
||||||
const effectiveWorkOrderId = task?.work_order ? String(task.work_order) : String(selectedWorkOrderId || '');
|
const effectiveWorkOrderId = task?.work_order ? String(task.work_order) : String(selectedWorkOrderId || '');
|
||||||
|
|
||||||
@@ -355,7 +364,7 @@ export default function TaskServiceRecordsModal({
|
|||||||
{currentWorkOrder && !showWorkOrderEditor ? (
|
{currentWorkOrder && !showWorkOrderEditor ? (
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2 rounded border border-border-hairline bg-canvas-elevated px-3 py-2">
|
<div className="flex flex-wrap items-center justify-between gap-2 rounded border border-border-hairline bg-canvas-elevated px-3 py-2">
|
||||||
<p className="text-sm text-text-main">
|
<p className="text-sm text-text-main">
|
||||||
{formatWorkOrderDisplayCode(currentWorkOrder)} • {currentWorkOrder.craneLabel || currentWorkOrder.craneInfo?.registration_number || '-'} • {currentWorkOrder.status || '-'}
|
{formatWorkOrderDisplayCode(currentWorkOrder)} • {formatDate(currentWorkOrder.date)} • {currentWorkOrder.craneLabel || currentWorkOrder.craneInfo?.registration_number || '-'} • {currentWorkOrder.status || '-'}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -374,14 +383,19 @@ export default function TaskServiceRecordsModal({
|
|||||||
className="w-full rounded-lg border border-border-hairline bg-canvas-elevated px-3 py-2 text-sm text-text-main disabled:opacity-60"
|
className="w-full rounded-lg border border-border-hairline bg-canvas-elevated px-3 py-2 text-sm text-text-main disabled:opacity-60"
|
||||||
>
|
>
|
||||||
<option value="">— Odaberi putni nalog —</option>
|
<option value="">— Odaberi putni nalog —</option>
|
||||||
{sortedWorkOrders.map((workOrder) => (
|
{sortedWorkOrders.map((workOrder) => {
|
||||||
|
const isToday = String(workOrder.date || '').slice(0, 10) === todayIso();
|
||||||
|
return (
|
||||||
<option key={workOrder.id} value={String(workOrder.id)}>
|
<option key={workOrder.id} value={String(workOrder.id)}>
|
||||||
{formatWorkOrderDisplayCode(workOrder)} • {workOrder.craneLabel || workOrder.craneInfo?.registration_number || '-'} • {workOrder.status || '-'}
|
{isToday ? '★ ' : ''}{formatWorkOrderDisplayCode(workOrder)} • {formatDate(workOrder.date)} • {workOrder.craneLabel || workOrder.craneInfo?.registration_number || '-'} • {workOrder.status || '-'}
|
||||||
</option>
|
</option>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</select>
|
</select>
|
||||||
<p className="text-xs text-text-muted">
|
<p className="text-xs text-text-muted">
|
||||||
{assigningWorkOrder ? 'Spremanje odabira putnog naloga...' : 'Odabir se automatski sprema u bazu.'}
|
{assigningWorkOrder
|
||||||
|
? 'Spremanje odabira putnog naloga...'
|
||||||
|
: 'Isti putni nalog možete dodijeliti više zadataka. Danas (★) su prikazani prvo.'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user