From 06321fc3d1cd983e2e662d95365ef77f202eeac9 Mon Sep 17 00:00:00 2001 From: mariomitte Date: Sat, 1 Aug 2026 07:15:39 +0200 Subject: [PATCH] feat: auto-odabir i datum u dropdownu putnih naloga za taskove MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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> --- docs/production-db-local-setup.md | 218 ++++++++++++++++++ .../components/dashboard/TaskCreateModal.jsx | 39 +++- .../dashboard/TaskServiceRecordsModal.jsx | 36 ++- 3 files changed, 276 insertions(+), 17 deletions(-) create mode 100644 docs/production-db-local-setup.md diff --git a/docs/production-db-local-setup.md b/docs/production-db-local-setup.md new file mode 100644 index 0000000..4f5bb3d --- /dev/null +++ b/docs/production-db-local-setup.md @@ -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 +``` diff --git a/frontend/src/components/dashboard/TaskCreateModal.jsx b/frontend/src/components/dashboard/TaskCreateModal.jsx index b1485d6..41b21e8 100644 --- a/frontend/src/components/dashboard/TaskCreateModal.jsx +++ b/frontend/src/components/dashboard/TaskCreateModal.jsx @@ -13,6 +13,17 @@ const INITIAL_FORM = { 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 }) { const [form, setForm] = useState(INITIAL_FORM); const [submitting, setSubmitting] = useState(false); @@ -40,8 +51,18 @@ export default function TaskCreateModal({ open, workOrders = [], contextCrane = setTemplateId(''); setTemplates([]); 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(() => { 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" > - {availableWorkOrders.map((workOrder) => ( - - ))} + {availableWorkOrders.map((workOrder) => { + const isToday = String(workOrder.date || '').slice(0, 10) === todayIso(); + return ( + + ); + })} + + Više zadataka može dijeliti isti putni nalog. Danas kreiran nalog označen je s ★. + {templateMode && ( Kod template unosa nalog nije obavezan i backend ga neće automatski povezati. diff --git a/frontend/src/components/dashboard/TaskServiceRecordsModal.jsx b/frontend/src/components/dashboard/TaskServiceRecordsModal.jsx index 4250dec..a05ad15 100644 --- a/frontend/src/components/dashboard/TaskServiceRecordsModal.jsx +++ b/frontend/src/components/dashboard/TaskServiceRecordsModal.jsx @@ -6,6 +6,10 @@ import { getTaskCraneDisplay, getTaskCraneOwner } from '../../lib/taskCraneDispl const STATUS_OPTIONS = ['aktivan', 'servis', 'spreman_za_zavrsetak', 'zavrsen', 'neaktivan']; +function todayIso() { + return new Date().toISOString().slice(0, 10); +} + function formatDate(value) { if (!value) return '-'; const parsed = new Date(value); @@ -79,10 +83,15 @@ export default function TaskServiceRecordsModal({ [workOrders, task?.work_order, selectedWorkOrderId] ); - const sortedWorkOrders = useMemo( - () => [...workOrders].sort((a, b) => String(b.date || '').localeCompare(String(a.date || ''))), - [workOrders] - ); + const sortedWorkOrders = useMemo(() => { + const today = todayIso(); + 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 || ''); @@ -355,7 +364,7 @@ export default function TaskServiceRecordsModal({ {currentWorkOrder && !showWorkOrderEditor ? (

- {formatWorkOrderDisplayCode(currentWorkOrder)} • {currentWorkOrder.craneLabel || currentWorkOrder.craneInfo?.registration_number || '-'} • {currentWorkOrder.status || '-'} + {formatWorkOrderDisplayCode(currentWorkOrder)} • {formatDate(currentWorkOrder.date)} • {currentWorkOrder.craneLabel || currentWorkOrder.craneInfo?.registration_number || '-'} • {currentWorkOrder.status || '-'}

)}